是否有更好的方法来重载ostream运算符<<?

时间:2019-12-21 17:52:42

标签: c++ operator-overloading c++17 iostream ostream

假设您具有以下代码:

#include <iostream>

template <typename T>
class Example
{
  public:
    Example() = default;
    Example(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele) { }

    friend std::ostream &operator<<(std::ostream &os, const Example &a)
    {
      return (os << a.first_ << " " << a.second_);
    }

  private:
    T first_;
    T second_;
};

int main()
{
  Example example_(3.45, 24.6); // Example<double> till C++14
  std::cout << example_ << "\n";
}

这是使operator<<重载的唯一方法吗?

friend std::ostream &operator<<(std::ostream &os, const Example &a)
{
  return (os << a.first_ << " " << a.second_);
}

就性能而言,这是重载它的最佳方法,还是有更好的选择来实现此效果?

4 个答案:

答案 0 :(得分:-1)

这是实现它的显而易见的方法。它可能也是最有效的。使用它。

答案 1 :(得分:-1)

我相信这些评论已经很好地回答了您的问题。从纯粹的性能角度来看,可能没有“更好”的方法来为输出流重载<<运算符,因为您的功能很可能并不是瓶颈。

我建议使用一种“更好”的方式来编写处理某些极端情况的函数本身。

您的<<重载(如现在存在)将在尝试执行某些输出格式设置操作时“中断”。

std::cout << std::setw(15) << std::left << example_ << "Fin\n";

这不会使整个Example输出对齐。相反,它仅左对齐first_成员。这是因为您一次将一项放入流中。 std::left将获取下一个要左对齐的项目,这只是类输出的一部分。

最简单的方法是构建一个字符串,然后将该字符串转储到您的输出流中。像这样:

friend std::ostream &operator<<(std::ostream &os, const Example &a)
{
    std::string tmp = std::to_string(a.first_) + " " + std::to_string(a.second_);
    return (os << tmp);
}

在这里需要注意几件事。第一个是在此特定示例中,您将得到尾随0,因为您无法控制std::to_string()格式化其值的方式。这可能意味着编写特定于类型的转换函数可以为您进行任何修整。您也许还可以使用string_views(以获取一些效率(再次,它可能并不重要,因为函数本身可能仍然不是您的瓶颈)),但是我对它们没有经验。

通过将对象的所有信息一次放入流中,该左对齐现在将对齐对象的全部输出。

还有关于朋友还是非朋友的争论。如果存在必要的吸气剂,我认为非朋友是必经之路。朋友非常有用,但由于它们是具有特殊访问权限的非成员函数,因此也会破坏封装。这进入了意见领域,但是除非我觉得简单的吸气剂是必要的,否则我不会编写简单的吸气剂,并且我也不认为有必要<<重载。

答案 2 :(得分:-1)

据我了解,这个问题提出了两个歧义点:

  1. 您是否专门针对模板化类。
    我认为答案是肯定的。

  2. 是否有更好的方法重载ostream operator<<(与friend相比),如问题标题中所述(和假设“更好”指的是性能),或者正文中有其他方式(“这是唯一的方式...”吗?)
    我将假设第一个,因为它包含第二个。

我构想了至少3种使ostream operator<<重载的方法:

  1. 您发布的friend方式。
  2. friend方式,返回类型为auto
  3. friend方式,返回类型为std::ostream

它们在底部被举例说明。 我进行了几次测试。通过所有这些测试(请参见下面的代码),我得出结论:

  1. 已经在优化模式下(使用-O3进行了编译/链接,并且每个std::cout循环了10000次,这3种方法基本上提供了相同的性能。

  2. 已在调试模式下编译/链接,没有循环

    t1 ~ 2.5-3.5 * t2
    t2 ~ 1.02-1.2 * t3
    


    即1比2慢得多,而3的表现相似。

我不知道这些结论是否适用于整个系统。 我也不知道您是否看到的行为接近于1(最可能)或接近2(在特定条件下)。


定义用于重载operator<<的三种方法的代码
(我删除了默认构造函数,因为它们在这里无关紧要。)

方法1(如在OP中):

template <typename T>
class Example
{
  public:
    Example(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele) { }

    friend std::ostream &operator<<(std::ostream &os, const Example &a)
    {
      return (os << a.first_ << " " << a.second_);
    }

  private:
    T first_;
    T second_;
};

方法2:

template <typename T>
class Example2
{
  public:
    Example2(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele) { }

    void print(std::ostream &os) const
    {
        os << this->first_ << " " << this->second_;
        return;
    }

  private:
    T first_;
    T second_;
};
template<typename T>
auto operator<<(std::ostream& os, const T& a) -> decltype(a.print(os), os)
{
    a.print(os);
    return os;
}

方法3:

template <typename T>
class Example3
{
  public:
    Example3(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele) { }

    void print(std::ostream &os) const
    {
        os << this->first_ << " " << this->second_;
        return;
    }

  private:
    T first_;
    T second_;
};
// Note 1: If this function exists, the compiler makes it take precedence over auto... above
// If it does not exist, code compiles ok anyway and auto... above would be used
template <typename T>
std::ostream &operator<<(std::ostream &os, const Example3<T> &a)
{
    a.print(os);
    return os;
}
// Note 2: Explicit instantiation is not needed here.
//template std::ostream &operator<<(std::ostream &os, const Example3<double> &a);
//template std::ostream &operator<<(std::ostream &os, const Example3<int> &a);

用于测试性能的代码
(所有内容都与

一起放在单个源文件中
#include <iostream>
#include <chrono>

在顶部):

int main()
{
    std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
    const int nout = 10000;

    Example example_(3.45, 24.6); // Example<double> till C++14
    begin = std::chrono::steady_clock::now();
    for (int i = 0 ; i < nout ; i++ )
        std::cout << example_ << "\n";
    end = std::chrono::steady_clock::now();
    const double lapse1 = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();
    std::cout << "Time difference = " << lapse1 << "[us]" << std::endl;

    Example2 example2a_(3.5, 2.6); // Example2<double> till C++14
    begin = std::chrono::steady_clock::now();
    for (int i = 0 ; i < nout ; i++ )
        std::cout << example2a_ << "\n";
    end = std::chrono::steady_clock::now();
    const double lapse2a = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();
    std::cout << "Time difference = " << lapse2a << "[us]" << std::endl;

    Example2 example2b_(3, 2); // Example2<double> till C++14
    begin = std::chrono::steady_clock::now();
    for (int i = 0 ; i < nout ; i++ )
        std::cout << example2b_ << "\n";
    end = std::chrono::steady_clock::now();
    const double lapse2b = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();
    std::cout << "Time difference = " << lapse2b << "[us]" << std::endl;

    Example3 example3a_(3.4, 2.5); // Example3<double> till C++14
    begin = std::chrono::steady_clock::now();
    for (int i = 0 ; i < nout ; i++ )
        std::cout << example3a_ << "\n";
    end = std::chrono::steady_clock::now();
    const double lapse3a = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();
    std::cout << "Time difference = " << lapse3a << "[us]" << std::endl;

    std::cout << "Time difference lapse1 = " << lapse1 << "[us]" << std::endl;
    std::cout << "Time difference lapse2a = " << lapse2a << "[us]" << std::endl;
    std::cout << "Time difference lapse2b = " << lapse2b << "[us]" << std::endl;
    std::cout << "Time difference lapse3a = " << lapse3a << "[us]" << std::endl;

    return 0;
}

答案 3 :(得分:-3)

您在问题中演示的方式是最基本的方式,各种C ++书籍中也都有这种方式。就我个人而言,我可能不喜欢我的生产代码,主要是因为:

  • 必须为每个类编写friend operator<<的样板代码。
  • 添加新的类成员时,您可能还必须分别更新方法。

从C ++ 14开始,我建议采用以下方式:

图书馆

// Add `is_iterable` trait as defined in https://stackoverflow.com/a/53967057/514235
template<typename Derived>
struct ostream
{
  static std::function<std::ostream&(std::ostream&, const Derived&)> s_fOstream;

  static auto& Output (std::ostream& os, const char value[]) { return os << value; }
  static auto& Output (std::ostream& os, const std::string& value) { return os << value; }
  template<typename T>
  static
  std::enable_if_t<is_iterable<T>::value, std::ostream&>
  Output (std::ostream& os, const T& collection)
  {
    os << "{";
    for(const auto& value : collection)
      os << value << ", ";
    return os << "}";
  }
  template<typename T>
  static
  std::enable_if_t<not is_iterable<T>::value, std::ostream&>
  Output (std::ostream& os, const T& value) { return os << value; }

  template<typename T, typename... Args>
  static
  void Attach (const T& separator, const char names[], const Args&... args)
  {
    static auto ExecuteOnlyOneTime = s_fOstream =
    [&separator, names, args...] (std::ostream& os, const Derived& derived) -> std::ostream&
    {
      os << "(" << names << ") =" << separator << "(" << separator;
      int unused[] = { (Output(os, (derived.*args)) << separator, 0) ... }; (void) unused;
      return os << ")";
    };
  }

  friend std::ostream& operator<< (std::ostream& os, const Derived& derived)
  {
    return s_fOstream(os, derived);
  }
};

template<typename Derived>
std::function<std::ostream&(std::ostream&, const Derived&)> ostream<Derived>::s_fOstream;

用法

为想要使用operator<<工具的那些类继承上述类。 friend会自动通过基数ostream包含在那些类的定义中。因此,无需额外的工作。例如

class MyClass : public ostream<MyClass> {...};

最好在其构造函数中,Attach()将要打印的成员变量。例如

// Use better displaying with `NAMED` macro
// Note that, content of `Attach()` will effectively execute only once per class
MyClass () { MyClass::Attach("\n----\n", &MyClass::x, &MyClass::y); }

示例

根据您分享的内容

#include"Util_ostream.hpp"

template<typename T>
class Example : public ostream<Example<T>> // .... change 1
{
public:
  Example(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele)
  {
    Example::Attach(" ", &Example::first_, &Example::second_); // .... change 2
  }

private:
  T first_;
  T second_;
};

Demo

此方法对变量的每次打印都具有指针访问权限,而不是直接访问。从性能的角度来看,这种可以忽略的间接调用永远不会成为代码中的瓶颈。
出于实际目的,演示要稍微复杂一些。

要求

  • 此处的目的是提高打印变量的可读性和一致性
  • 每个可打印类都应具有各自的ostream<T>,而与继承无关
  • 对象应该定义operator<<或继承ostream<T>才能进行编译

设施

现在它已成为一个很好的库组件。到目前为止,我已经添加了以下附加功能。

  • 使用ATTACH()宏,我们还可以按某些方式打印变量;始终可以根据需要通过修改库代码来自定义可变打印
  • 如果基类是可打印的,那么我们可以简单地传递一个类型转换的this;休息会得到照顾
  • 现在支持具有std::begin/end兼容性的容器,其中包括vectormap

为了快速理解,开头所示的代码较短。那些有兴趣的人可以单击上面的演示链接。