【问题标题】:C++ expression SFINAE and ostream manipulatorsC++ 表达式 SFINAE 和 ostream 操纵器
【发布时间】:2020-01-30 06:30:52
【问题描述】:

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

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

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

最好不要启用无法通过operator&lt;&lt; 打印的structclass 模板。 但是,使用此 SFINAE,ostream 操纵器无法正常工作……而且我不知道为什么。

错误和我的期望:

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

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

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

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

【问题讨论】:

  • 请提供完整的工作示例。您的 CustomOutput 课程目前缺少某些部分
  • @NutCracker 抱歉。我添加了构造函数和成员。

标签: c++ templates c++17 sfinae


【解决方案1】:

首先,修复您的 ostreamable 类。目前,您的课程要求 T 可以从 0 复制构造。这不是很多类的情况。它应该使用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&lt;T&gt;() 创建T 类型的对象。 std::declval&lt;T&gt; 是一个(故意未定义的)函数模板,当用于未计算的操作数(例如 decltypesizeofnoexcept 运算符等)时,它会生成类型为 T 的对象,而没有依赖关系在特定的构造签名上(在您的情况下从 0 复制构造)。

  • check 的参数被替换为intvalue 变量的初始化器使用参数0 调用check,所以这个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);

对于要使用的函数模板,必须确定适当的模板参数。无法将 type-template 参数T 推导出为泛型模板。

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

【讨论】:

  • 感谢您的回答。我能够让std::endl 以这种方式工作。但是,std::setprecision(int) 之类的东西不起作用。我的目标是创建一个支持用户定义类的自定义格式化程序。意思是,我只需向operator&lt;&lt; 添加一些重载。在这种情况下继承std::ostream 会更好吗?我还在学习什么时候适合使用模板。
  • @funnypigrun 您的 ostreamable 类已损坏。我会在一分钟内更新答案以解决这个问题。像std::setprecision 这样的“常规”操纵器不需要特殊处理。
  • 哦。让我确保我理解正确。所以在我的代码中,我写了check(T t),将t 作为正式参数。这导致该函数在该行后面评估 std::declval&lt;T&gt;() 时需要一个副本?
  • @funnypigrun 你的函数将T t 作为参数,调用它时你写了check&lt;O&gt;(0) 传递0 作为参数。这意味着它只有在O 有一个接受0 作为参数的构造函数或者0 可以转换为O 或者Oint 时才有效。
  • @funnypigrun super 是对的。我已经更新了我的答案来解决这个问题。
【解决方案2】:

我知道已经有一个公认的答案,但我想提一下使用概念做同样事情的更漂亮的 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 之前相同的方式解决,方法是为它们提供特殊的重载。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-12
    • 2021-09-09
    相关资源
    最近更新 更多