C++ 表达式 SFINAE 和 ostream 操纵器

C++ expression SFINAE and ostream manipulators

我正在尝试学习如何使用 SFINAE。 出于练习目的,我试图制作一个 std::ostream 包装器以制作自定义格式化程序。

这是我的 SFINAE 和自定义输出 class。

// Tester
template <class O>
struct is_ostreamable {
    template <class T>
    static auto check(T t) -> decltype(std::declval<std::ostream &>() << t, std::true_type());
    template <class>
    static auto check(...) -> std::false_type;

  public:
    static constexpr bool value{std::is_same_v<decltype(check<O>(0)), std::true_type>};
};

// Custom class
struct CustomOutput {
    // Constructor etc...
    CustomOutput(std::ostream &os = std::cout) : os{os} {}
    std::ostream &os;

    // Problematic template function
    template <class O, class = std::enable_if_t<is_ostreamable<O>::value>>
    CustomOutput &operator<<(O o) {
        os << o;
        return *this;
    }
};

structclass 无法通过 operator<< 打印的模板不启用是完美的。 但是,对于这个 SFINAE,ostream 操纵器不工作......我不明白为什么。

错误,以及我的期望:

int main(void){
    CustomOutput{} << "hi"; // Fine

    std::vector<int> vec;
    // CustomOutput{} << vec; // Error. Expected

    CustomOutput{} << std::endl; // Error. WHY?
}

也许我错过了什么?任何帮助将不胜感激。

首先,修复您的 ostreamable class。目前,您的 class 要求 T 可以从 0 复制构造。许多 classes 并非如此。它应该使用 std::declval 来创建值:

template <class O>
struct is_ostreamable {
    template <class T>
    static auto check(int) -> decltype(std::declval<std::ostream &>() << std::declval<T>(), std::true_type());
    template <class>
    static auto check(...) -> std::false_type;

public:
    static constexpr bool value{std::is_same_v<decltype(check<O>(0)), std::true_type>};
};

这里做了两处改动:

  • decltype 的操作数使用 std::declval<T>() 创建类型 T 的对象。 std::declval<T> 是一个(故意未定义的)函数模板,当在未计算的操作数(例如 decltypesizeofnoexcept 运算符等)而不依赖于特定的构造签名(在您的情况下,从 0 复制构造)。

  • check 的参数替换为 intvalue 变量的初始化器调用 check 参数 0,所以这个 int 参数确保 (int) 在重载中排名高于 (...)分辨率,以便尽可能选择 true_type 重载。


您需要为函数式操纵器(std::endlstd::flush 等)提供特殊重载:

using manip = std::ostream& (*)(std::ostream&);

CustomOutput& operator<<(manip m) {
    os << m;
    return *this;
}

遗憾的是,无法使通用模板版本支持此功能。这是因为 std::endl 是函数模板:

template <class CharT, class Traits>
std::basic_ostream<CharT, Traits>& endl(td::basic_ostream<CharT, Traits>& os);

要使用函数模板,必须确定适当的模板参数。无法将类型模板参数 T 推断为通用模板。

无论如何,这可能是您唯一需要的特殊重载。

我知道已经有一个可接受的答案,但我想提一下使用概念做同样事情的更漂亮的 C++20 方法:

#include <iostream>
#include <concepts>

using OManipulator= std::ostream&(&)(std::ostream &);

template <typename T>
concept OStreamable = requires(T t) {
    std::declval<std::ostream&>() << t;
};

struct CustomOutput {
    std::ostream &os;

    CustomOutput(std::ostream &os = std::cout)
        : os{os}
    {}

    template <typename T> requires OStreamable<T>
    CustomOutput& operator<<(T out) {
        os << out;
        return *this;
    }

    CustomOutput& operator<<(OManipulator out) {
        os << out;
        return *this;
    }
};

int main(void){
    CustomOutput{} << "hello";
    CustomOutput{} << std::endl;
    CustomOutput{} << "world";
}

基本上,在 C++20 中,需要以与 C++20 之前的版本相同的方式解决操纵器问题,方法是为它们提供特殊的重载。