在 C++17 中使用 SFINAE 和模板的 "std::ostream" 和“<<”运算符的回退
Fallback for "std::ostream" and "<<" operator using SFINAE and templates in C++17
我在内部使用 Catch2 和 TEST_CASE
块 我有时为了方便起见声明本地临时 struct
。这些 struct
有时需要显示,为此 Catch2 建议用 std::ostream
实现 <<
运算符。不幸的是,使用 local-only 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 优先于模板(如果存在)?
另外,我发现这个解决方案非常脆弱(尽管这个简单的片段有效,但在编译我的整个项目时我会出错),而且我认为由于其隐含的性质,它可能会导致错误。不相关的 classes 可能会定义 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
方法,否则它会阻止聚合初始化(即 super-useful 对于我的测试用例)。因此,WithToString
只是一个空 struct
,用作 std::enable_if
的“标记”。它本身并没有带来任何有用的信息,需要文档才能正确理解和使用。
您对第二种解决方案有何看法?这可以以某种方式改进吗?
我的目标是 C++17,所以很遗憾我还不能使用 <concepts>
。我也想避免使用 <experimental>
header (虽然我知道它包含对 C++17 有用的东西)。
您可以将这两种方法视为“operator<<
对所有类型都有一些 属性”。
第一个 属性 是“有一个 toString()
”方法(甚至可以在 C++11 中工作。这仍然是 SFINAE,在这种情况下,替换在 return 类型)。你可以让它检查 toString()
return 是一个 std::string
与不同风格的 SFINAE:
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;
}
而 non-template operator<<
将始终在该模板之前被选中。在此之前,还将选择一个更“专业”的模板。重载决议的规则有点复杂,但可以在这里找到:https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function
第二个属性是“源自WithToString
”。如您所料,这个更“明确”,accidentally/unexpectedly 使用 operator<<
.
更难
您实际上可以使用友元函数定义内联运算符:
struct A {
std::string toString() const {
return "A";
}
friend std::ostream& operator<<(std::ostream& os, const A& a) {
return os << a.toString();
}
};
你也可以在 WithToString
中有这个朋友声明,使其成为 self-documenting mixin
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";
}
};
我在内部使用 Catch2 和 TEST_CASE
块 我有时为了方便起见声明本地临时 struct
。这些 struct
有时需要显示,为此 Catch2 建议用 std::ostream
实现 <<
运算符。不幸的是,使用 local-only 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 优先于模板(如果存在)?
另外,我发现这个解决方案非常脆弱(尽管这个简单的片段有效,但在编译我的整个项目时我会出错),而且我认为由于其隐含的性质,它可能会导致错误。不相关的 classes 可能会定义 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
方法,否则它会阻止聚合初始化(即 super-useful 对于我的测试用例)。因此,WithToString
只是一个空 struct
,用作 std::enable_if
的“标记”。它本身并没有带来任何有用的信息,需要文档才能正确理解和使用。
您对第二种解决方案有何看法?这可以以某种方式改进吗?
我的目标是 C++17,所以很遗憾我还不能使用 <concepts>
。我也想避免使用 <experimental>
header (虽然我知道它包含对 C++17 有用的东西)。
您可以将这两种方法视为“operator<<
对所有类型都有一些 属性”。
第一个 属性 是“有一个 toString()
”方法(甚至可以在 C++11 中工作。这仍然是 SFINAE,在这种情况下,替换在 return 类型)。你可以让它检查 toString()
return 是一个 std::string
与不同风格的 SFINAE:
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;
}
而 non-template operator<<
将始终在该模板之前被选中。在此之前,还将选择一个更“专业”的模板。重载决议的规则有点复杂,但可以在这里找到:https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function
第二个属性是“源自WithToString
”。如您所料,这个更“明确”,accidentally/unexpectedly 使用 operator<<
.
您实际上可以使用友元函数定义内联运算符:
struct A {
std::string toString() const {
return "A";
}
friend std::ostream& operator<<(std::ostream& os, const A& a) {
return os << a.toString();
}
};
你也可以在 WithToString
中有这个朋友声明,使其成为 self-documenting mixin
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";
}
};