【问题标题】:declval expression (for SFINAE) with std::ostream带有 std::ostream 的 declval 表达式(用于 SFINAE)
【发布时间】:2016-05-27 10:13:35
【问题描述】:

我正在尝试创建一个类型特征类以确定特定类型 T 是否可以通过 std::ostream<< 运算符进行流式传输。我使用的是简单的 SFINAE 技术。

最终,我尝试评估替换失败的表达式是:

decltype(std::declval<std::ostream>() << std::declval<T>()) ;

我的期望是,给定一个T 类型的实例t 和一个std::ostream 实例os,如果表达式os &lt;&lt; t 格式不正确,那么应该会发生替换失败。

但显然,无论T 类型如何,这里都不会发生替换失败。即使我只是使用上面的decltype 表达式声明了一个typedef,在SFINAE 的上下文之外,它也会愉快地编译,即使T 不能与std::ostream 一起使用。

例如:

struct Foo  { };

int main()
{
    // This compiles fine using GCC 4.9.2
    //
    typedef decltype(
        std::declval<std::ostream>() << std::declval<Foo>()
    ) foo_type;
}

上面的代码使用 GCC 4.9.2 可以很好地编译,这不是我所期望的,因为 &lt;&lt; 运算符没有重载以使用类型 Foo。当然,如果我说:

std::cout << Foo();

... 我得到一个编译器错误。那么为什么上面的decltype 表达式甚至可以编译呢?

【问题讨论】:

  • 它与std::declval&lt;std::ostream&amp;&gt;() 正确反应。但我不知道为什么。

标签: c++ sfinae


【解决方案1】:

C++11 添加了以下operator&lt;&lt; 重载:

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os, 
                                            const T& value );

这将转发到标准插入运算符,它不能将右值引用绑定到std::ostreams,因为它们采用非常量引用。由于std::declval&lt;std::ostream&gt; 返回std::ostream&amp;&amp;,因此选择了此重载,然后由于非常宽松的接口(即,如果没有有效的底层插入运算符,这不是SFINAEd),您的decltype 说明符有效。

简单的解决方法是使用std::declval&lt;std::ostream&amp;&gt;()。这将返回一个std::ostream&amp;,因此您的decltype 说明符不会选择模板重载,并且需要正常的插入运算符重载才能编译:

typedef decltype(
    std::declval<std::ostream&>() << std::declval<Foo>()
) foo_type;

Clang 输出如下:

main.cpp:8:39: error: invalid operands to binary expression ('std::basic_ostream<char>' and 'Foo')
        std::declval<std::ostream&>() << std::declval<Foo>()
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^  ~~~~~~~~~~~~~~~~~~~

Live Demo


这是一个更简单的例子,它展示了同样的问题:

#include <string>

void foo (int&,int){}
void foo (int&,float){}

template <typename T>
void foo (int&& a, T b) {
    foo(a, b);
}

int main()
{
    std::string s;
    typedef decltype(foo(1,s)) foo_type;
}

Live Demo


以下是相关标准报价(N4140):

声明必须实例化,因为涉及重载解析:

[temp.inst]/10: 如果以涉及重载的方式使用函数模板或成员函数模板特化 分辨率,特化的声明被隐式实例化(14.8.3)。

只有声明需要实例化:

[temp.over]/5: 只需要函数模板特化的签名就可以将特化输入一组 候选函数。因此,只需要函数模板声明来解析调用 模板专业化是候选者。

并且实现不允许实例化函数体:

[temp.inst]/11: 实现不应隐式实例化不需要实例化的类模板的函数模板、变量模板、成员模板、非虚拟成员函数、成员类或静态数据成员。

【讨论】:

  • 为什么您的简单示例会出现问题? (即,为什么不是替换失败?)
  • @Eric 我的意思是它在任何情况下都不会是替换失败,因为它不在 SFINAE 上下文中,但这不是硬失败,因为 decltype 说明符是未评估的上下文,所以只有声明被实例化。
  • @Eric 我添加了相关标准的引用,这样更清楚吗?
【解决方案2】:

并没有真正回答为什么会发生这种情况,但如果你用std::stream&amp; 替换如下:

template<typename T, typename Enable = std::ostream&>
struct can_be_streamed : std::false_type {};
template<typename T>
struct can_be_streamed<T, 
         decltype(std::declval<std::ostream&>() << std::declval<T>())> : std::true_type {};

似乎有效。

Live Demo

【讨论】:

    【解决方案3】:

    如果你查看头文件ostream,你会发现因为std::declval产生了rvlaue引用,其实有一个匹配的泛型operator&lt;&lt;

    #if __cplusplus >= 201103L
      /** 
       *  @brief  Generic inserter for rvalue stream
       *  @param  __os  An input stream.
       *  @param  __x  A reference to the object being inserted.
       *  @return  os  
       *   
       *  This is just a forwarding function to allow insertion to
       *  rvalue streams since they won't bind to the inserter functions
       *  that take an lvalue reference.
      */  
      template<typename _CharT, typename _Traits, typename _Tp>
        inline basic_ostream<_CharT, _Traits>&
        operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
        {   
          __os << __x;
          return __os;
        }   
    #endif // C++11
    

    这解释了为什么您没有出现替代失败。但是,这实际上不能与 std::cout &lt;&lt; Foo() 调用匹配。以下是编译错误的相关部分:

    /usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/6.1.0/../../../../include/c++/6.1.0/ostream:628:5: note: candidate function [with _CharT = char, _Traits = std::char_traits<char>, _Tp = Foo] not viable: no known conversion from 'ostream' (aka 'basic_ostream<char>') to 'basic_ostream<char, std::char_traits<char> > &&' for 1st argument
        operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
        ^
    

    这里的问题是 lhs 只能是一个右值引用,但你(显然)在调用中使用了一个左值(即std::cout)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-09-17
      • 1970-01-01
      相关资源
      最近更新 更多