有没有更好的方法来重载 ostream operator<<?
Are there better ways to overload ostream operator<<?
假设您有以下代码:
#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_);
}
就性能而言,它是重载它的最佳方式还是有更好的选择来执行此实现?
这是实现它的显而易见的方法。它也可能是最有效的。使用它。
你在题中演示的方式是最基本的方式,在各种C++书籍中也能找到。就个人而言,我可能不喜欢我的生产代码,主要是因为:
- 必须为每个 class 编写
friend operator<<
的样板代码。
- 添加新 class 成员时,您可能还需要单独更新方法。
从 C++14 开始,我建议采用以下方式:
图书馆
// Add `is_iterable` trait as defined in
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<<
设施的那些 class 继承上述 class。 friend
将自动通过基础 ostream
包含在那些 class 的定义中。所以没有额外的工作。例如
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
这种方法在每次打印变量时都有一个指针访问,而不是直接访问。从性能的角度来看,这种可以忽略不计的间接访问绝不应该成为代码中的瓶颈。
出于实际目的,演示稍微复杂一些。
要求
- 这里的目的是提高打印变量的可读性和一致性
- 无论继承如何,每个可打印的 class 都应该有各自的
ostream<T>
- 对象应该
operator<<
定义或ostream<T>
继承才能编译
设施
这正在成为一个很好的库组件。以下是附加设施,我到目前为止已经添加了。
- 使用
ATTACH()
宏,我们也可以通过某种方式打印变量;通过修改库代码可以随时根据需要自定义可变打印
- 如果基础 class 是可打印的,那么我们可以简单地传递一个类型转换的
this
;会注意休息
- 现在支持具有
std::begin/end
兼容性的容器,其中包括 vector
以及 map
为了快速理解,开头显示的代码较短。有兴趣的可以点击上面的演示link
我相信评论已经很好地回答了你的问题。从纯粹的性能角度来看,可能没有 "better" 方法来重载输出流的 <<
运算符,因为您的函数可能不是瓶颈。
我会建议有一种 "better" 方法来编写处理某些特殊情况的函数本身。
您的 <<
重载,因为它现在存在,将 'break' 在尝试执行某些输出格式化操作时。
std::cout << std::setw(15) << std::left << example_ << "Fin\n";
这不会左对齐整个 Example
输出。相反,它只左对齐 first_
成员。这是因为您一次将一个项目放入流中。 std::left
将抓住下一个左对齐的项目,这只是 class 输出的一部分。
最简单的方法是构建一个字符串,然后将该字符串转储到您的输出流中。像这样:
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()
如何格式化其值。这可能意味着编写特定于类型的转换函数来为您进行任何调整。您也可以使用 std::string_views
(以提高效率(同样,这可能并不重要,因为函数本身可能仍然不是您的瓶颈)),但我没有使用它们的经验。
通过一次将对象的所有信息放入流中,左对齐现在将对齐对象的完整输出。
还有朋友和非朋友的争论。如果存在必要的吸气剂,我认为非朋友是可行的方法。友元很有用,但也会破坏封装,因为它们是具有特殊访问权限的非成员函数。这进入了意见领域,但我不会编写简单的 getter,除非我觉得它们是必要的,而且我不认为 <<
重载是必要的。
据我了解,这个问题有两个歧义点:
是否专门针对模板化类。
我假设答案是肯定的。
是否有 更好的方法 来重载 ostream operator<<
(与 friend
方式相比),如在问题的标题(假设 "better" 指的是性能),或者有 其他方式 ,如 body ("Is this the only way..."? )
我将假设第一个,因为它包含第二个。
我设想了至少 3 种方法来重载 ostream operator<<
:
friend
方式,正如您发布的那样。
- 非
friend
方式,auto
return类型。
- 非
friend
方式,用std::ostream
return类型。
它们在底部举例说明。
我运行几次测试。从所有这些测试(见下面用于测试的代码),我得出结论:
compiled/linked 处于优化模式(-O3
),每个循环 10000 次 std::cout
,所有 3 种方法提供基本相同的性能。
在调试模式下 compiled/linked,没有循环
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_;
};
方法二:
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;
}
方法三:
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;
}
假设您有以下代码:
#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_);
}
就性能而言,它是重载它的最佳方式还是有更好的选择来执行此实现?
这是实现它的显而易见的方法。它也可能是最有效的。使用它。
你在题中演示的方式是最基本的方式,在各种C++书籍中也能找到。就个人而言,我可能不喜欢我的生产代码,主要是因为:
- 必须为每个 class 编写
friend operator<<
的样板代码。 - 添加新 class 成员时,您可能还需要单独更新方法。
从 C++14 开始,我建议采用以下方式:
图书馆
// Add `is_iterable` trait as defined in
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<<
设施的那些 class 继承上述 class。 friend
将自动通过基础 ostream
包含在那些 class 的定义中。所以没有额外的工作。例如
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
这种方法在每次打印变量时都有一个指针访问,而不是直接访问。从性能的角度来看,这种可以忽略不计的间接访问绝不应该成为代码中的瓶颈。
出于实际目的,演示稍微复杂一些。
要求
- 这里的目的是提高打印变量的可读性和一致性
- 无论继承如何,每个可打印的 class 都应该有各自的
ostream<T>
- 对象应该
operator<<
定义或ostream<T>
继承才能编译
设施
这正在成为一个很好的库组件。以下是附加设施,我到目前为止已经添加了。
- 使用
ATTACH()
宏,我们也可以通过某种方式打印变量;通过修改库代码可以随时根据需要自定义可变打印 - 如果基础 class 是可打印的,那么我们可以简单地传递一个类型转换的
this
;会注意休息 - 现在支持具有
std::begin/end
兼容性的容器,其中包括vector
以及map
为了快速理解,开头显示的代码较短。有兴趣的可以点击上面的演示link
我相信评论已经很好地回答了你的问题。从纯粹的性能角度来看,可能没有 "better" 方法来重载输出流的 <<
运算符,因为您的函数可能不是瓶颈。
我会建议有一种 "better" 方法来编写处理某些特殊情况的函数本身。
您的 <<
重载,因为它现在存在,将 'break' 在尝试执行某些输出格式化操作时。
std::cout << std::setw(15) << std::left << example_ << "Fin\n";
这不会左对齐整个 Example
输出。相反,它只左对齐 first_
成员。这是因为您一次将一个项目放入流中。 std::left
将抓住下一个左对齐的项目,这只是 class 输出的一部分。
最简单的方法是构建一个字符串,然后将该字符串转储到您的输出流中。像这样:
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()
如何格式化其值。这可能意味着编写特定于类型的转换函数来为您进行任何调整。您也可以使用 std::string_views
(以提高效率(同样,这可能并不重要,因为函数本身可能仍然不是您的瓶颈)),但我没有使用它们的经验。
通过一次将对象的所有信息放入流中,左对齐现在将对齐对象的完整输出。
还有朋友和非朋友的争论。如果存在必要的吸气剂,我认为非朋友是可行的方法。友元很有用,但也会破坏封装,因为它们是具有特殊访问权限的非成员函数。这进入了意见领域,但我不会编写简单的 getter,除非我觉得它们是必要的,而且我不认为 <<
重载是必要的。
据我了解,这个问题有两个歧义点:
是否专门针对模板化类。
我假设答案是肯定的。是否有 更好的方法 来重载
ostream operator<<
(与friend
方式相比),如在问题的标题(假设 "better" 指的是性能),或者有 其他方式 ,如 body ("Is this the only way..."? )
我将假设第一个,因为它包含第二个。
我设想了至少 3 种方法来重载 ostream operator<<
:
friend
方式,正如您发布的那样。- 非
friend
方式,auto
return类型。 - 非
friend
方式,用std::ostream
return类型。
它们在底部举例说明。 我运行几次测试。从所有这些测试(见下面用于测试的代码),我得出结论:
compiled/linked 处于优化模式(
-O3
),每个循环 10000 次std::cout
,所有 3 种方法提供基本相同的性能。在调试模式下 compiled/linked,没有循环
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_;
};
方法二:
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;
}
方法三:
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;
}