在 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;
}

我有几个问题:

另外,我发现这个解决方案非常脆弱(尽管这个简单的片段有效,但在编译我的整个项目时我会出错),而且我认为由于其隐含的性质,它可能会导致错误。不相关的 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";
    }
};