在 C++17 中使用 SFINAE 和模板的“std::ostream”和“<<”运算符的后备

Del*_*gan 3 c++ sfinae type-traits template-meta-programming 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;
}
Run Code Online (Sandbox Code Playgroud)

我有几个问题:

  • 这个技巧是decltype现代 C++ 还是我们可以使用它<type_traits>来实现相同的效果?
  • 有没有办法要求toString()返回值为 astd::string从而禁用模板替换?
  • 是否保证具有具体实现的类operator<<将优先于模板(如果存在)?

另外,我发现这个解决方案非常脆弱(尽管这个简单的代码片段有效,但在编译整个项目时我遇到了错误),而且我认为由于其隐含的性质,它可能会导致错误。不相关的类可能会定义toString()方法而不期望在模板替换中使用它<<

我认为使用基类然后使用 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;
}
Run Code Online (Sandbox Code Playgroud)

该解决方案的缺点是我无法在基类中定义toString()virtual方法,否则它会阻止聚合初始化(这对我的测试用例非常有用)。因此,WithToString只是一个空的struct,充当 的“标记” std::enable_if。它本身不会带来任何有用的信息,并且需要文档才能正确理解和使用。

您对第二个解决方案有何看法?这可以以某种方式改进吗?

我的目标是 C++17,所以不幸的是我还不能使用<concepts>。另外我想避免使用<experimental>标头(尽管我知道它包含对 C++17 有用的东西)。

Art*_*yer 5

您可以将这两种方法视为“operator<<在具有某些属性的所有类型上”。

第一个属性是“has a toString()”方法(甚至可以在 C++11 中使用。这仍然是 SFINAE,在这种情况下,替换位于返回类型中)。您可以让它检查toString()返回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;
}
Run Code Online (Sandbox Code Playgroud)

operator<<并且始终会在该模板之前选择非模板。在此之前还会选择一个更“专业”的模板。重载解析的规则有点复杂,但可以在这里找到:https ://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function

第二个属性是“源自WithToString”。正如您所猜测的,这个更加“明确”,并且更难意外/意外地使用operator<<.

您实际上可以使用友元函数内联定义运算符:

struct A {
    std::string toString() const {
      return "A";
    }
    friend std::ostream& operator<<(std::ostream& os, const A& a) {
        return os << a.toString();
    }
};
Run Code Online (Sandbox Code Playgroud)

你也可以在 中包含这个朋友声明WithToString,使其成为一个自记录的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";
    }
};
Run Code Online (Sandbox Code Playgroud)