[英]Fallback for "std::ostream" and "<<" operator using SFINAE and templates in C++17
我在内部使用 Catch2 和TEST_CASE
块 我有时为了方便起见声明本地临时struct
。 有时需要显示这些struct
,为此 Catch2 建议使用std::ostream
实现<<
运算符。 不幸的是,使用仅限本地的struct
实现起来变得非常复杂,因为这样的运算符既不能内联定义,也不能在TEST_CASE
块中定义。
我想到了一个可能的解决方案,即为<<
定义一个模板,如果该方法存在,它将调用toString()
代替:
#include <iostream>
#include <string>
template <typename T>
auto operator<<(std::ostream& out, const T& obj) -> decltype(obj.toString(), void(), out)
{
out << obj.toString();
return out;
}
struct A {
std::string toString() const {
return "A";
}
};
int main() {
std::cout << A() << std::endl;
return 0;
}
我有几个问题:
decltype
技巧是现代的 C++ 还是我们可以使用<type_traits>
来实现同样的效果?toString()
返回值是一个std::string
从而禁用模板替换?operator<<
具体实现的 class 优先于模板(如果存在)? 此外,我发现这个解决方案非常脆弱(尽管这个简单的片段有效,但在编译我的整个项目时我会出错),而且我认为由于其隐含的性质,它可能会导致错误。 不相关的类可能会定义toString()
方法而不期望它被用于<<
模板替换。
我认为使用基数 class 然后使用 SFINAE 明确地执行此操作可能更清晰:
#include <iostream>
#include <string>
#include <type_traits>
struct WithToString {};
template <typename T, typename = std::enable_if_t<std::is_base_of_v<WithToString, T>>>
std::ostream& operator<<(std::ostream& out, const T& obj)
{
out << obj.toString();
return out;
}
struct A : public WithToString {
std::string toString() const {
return "A";
}
};
int main() {
std::cout << A() << std::endl;
return 0;
}
这个解决方案的缺点是我不能将toString()
定义为基类 class 中的virtual
方法,否则它会阻止聚合初始化(这对我的测试用例非常有用)。 因此, WithToString
只是一个空struct
,用作std::enable_if
的“标记”。 它本身不会带来任何有用的信息,需要文档才能正确理解和使用。
您对第二种解决方案有何看法? 这可以以某种方式改进吗?
我的目标是 C++17,所以不幸的是我还不能使用<concepts>
。 我也想避免使用<experimental>
header(尽管我知道它包含对 C++17 有用的东西)。
您可以将这两种方法视为“具有某些属性的所有类型上的operator<<
”。
第一个属性是“有一个toString()
”方法(甚至可以在 C++11 中工作。这仍然是 SFINAE,在这种情况下,替换在返回类型中)。 您可以检查toString()
是否返回具有不同 SFINAE 样式的std::string
:
template <typename T, std::enable_if_t<
std::is_same_v<std::decay_t<decltype(std::declval<const T&>().toString())>, std::string>,
int> = 0>
std::ostream& operator<<(std::ostream& out, const T& obj)
{
out << obj.toString();
return out;
}
在这个模板之前总是会选择一个非模板operator<<
。 在此之前,还将选择一个更“专业”的模板。 重载决议的规则有点复杂,但可以在这里找到: https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function
第二个属性是“派生自WithToString
”。 如您所料,这个更“明确”,并且更难意外/意外地使用operator<<
。
您实际上可以与朋友 function 一起定义内联运算符:
struct A {
std::string toString() const {
return "A";
}
friend std::ostream& operator<<(std::ostream& os, const A& a) {
return os << a.toString();
}
};
你也可以在WithToString
中有这个朋友声明,使它成为一个自我记录的混合
template<typename T> // (crtp class)
struct OutputFromToStringMixin {
friend std::ostream& operator<<(std::ostream& os, const T& obj) {
return os << obj.toString();
}
};
struct A : OutputFromToStringMixin<A> {
std::string toString() const {
return "A";
}
};
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.