在VS 2015 Update 3 中对SFINAE表达式的改进

在VS 2015 Update 3 中对SFINAE表达式的改进

 

[原文发表地址] Expression SFINAE improvements in VS 2015 Update 3

[原文发表时间] 2016/6/7

 

我们在去年12月发布了一篇关于在VS2015 Update 1中部分支持SFINAE表达式的博客。在那篇博客发布之后我们收到了许多希望更全面支持SFINAE表达式的诉求,尤其是在BoostRange-v3这两个被广泛使用的类库上。在过去的几个月里我们重点关注了这些类库。

 

什么是SFINAE?

提醒一下,SFINAE是‘Substitution Failure Is Not An Error’的缩写。其理念是当编译器尝试在重载解析时去特化一个函数模板,即使特化失败,只要还有其他的有效对象就可以。C++11介绍了诸如像decltype和constexpr的功能,并且在模板参数推导和替换过程中有表达式更为普遍。C++标准委员会阐明了C++11中SFINAE表达式的规则。

 

Boost库上的支持 Boost现在可以在不定义宏BOOST_NO_SFINAE_EXPR的情况下用MSVC来正确编译, 这是此次更新的一大亮点。我们与Boost的社区和维护人员一起来确保这是正确的。不过在Boost之上构建的类库和开源工程仍有一些阻塞问题。如果你遇到任何问题,请告知我们。你可以通过 visualcpp@microsoft.com来给我们发邮件,或者使用下面任意反馈渠道来联系我们。

 

在用MSVC编译时,Boost测试套中仍然存在3个失败的测试用例。两个与在模板类型参数中使用noexcept操作符有关。目前我们是直接解析它,即便其处于依赖环境中。还有一个是关于Boost类库的问题, 我们已经将该问题知会给维护人员。

 

Range-v3库上的支持

在我们测试Range-v3类库时,我们的改进并不像Boost库一样完善。我们审查了所有的range-v3的测试并且发现了许多问题。我们在内部记录了50个bugs来追踪我们的进展,并且修复了大约50%的bug。不过仍有一些问题遗留在了许多不同的功能区中,如name lookup,friend functions,variadic templates,alias templates,default template arguments,constexpr and noexcept。其他的问题将会在下面的 还要做什么 中列举。

 

Update 1之后的改进

尽管我们的实施仍只是一部分,但是我们差不多已经做完了。自Update 1以来我们已取得了很多进展。首先,让我们来讨论一下为什么让SFINAE表达式在MSVC编译器中工作是非常困难的。

 

MSVC的标记流解析器

从c++ 模板出现很久以前MSVC编译器就以已有些时日了—我们目前正在逐步实现一个非常有意义的设计方案。MSVC 通常是基于标记流来解析模板。

当我们在你的代码中遇到模板时,在不需要了解其具体含义的情况下,我们会捕获其主体来作为一组标记序列,并将其主体作为标记流进行存储, 这将会在解析一些包含decltype-specifiers的结尾返回类型时出现偏差,这种情况尤其发生在SFINAE 环境下。

 

我们当前正在实施一种向下递归的解析器,这种解析器可以为表达式产生更高级别的自由树,我们可以以更加精准的方式来用这个解析树来解析decltype 的表达式参数,这将会有助于我们更好的实施SFINAE 表达式。这个向下递归的解析器目前是一个正在进行中的工作; 当下它只能解析C++表达式, 但是我们会很快将其拓展到解析整个C++ 语法, 这也将是实施诸如两个阶段名查找等功能的基础。这些功能几乎已经不可能用标记流解析器来实现, 随着工作进展, 在SFINAE 表达式中遗留的空白也将会被逐渐填补。

如果你想要了解更多关于我们在解析器上所做的修改,你可以看看这篇博客:更新Microsoft C/C++ 编译器

我们当前的工作是什么?

我们目前正在为decltype表达式生成一些可以在Update 3上正确运行的解析树。

      • 我们已经实现了在编译器用新的解析树来检查dependent expression。它修复了在一个失败编译Chromium的链接问题

      • 我们已经实现了用解析树来区分在decltype。这里是一个从Boost线程库中简化的例子:

        template<class T>
        struct remove_reference
        {
            typedef T type;
        };
        
        template<class T>
        inline T&& forward(typename remove_reference<T>::type& t)
        {
           return static_cast<T&&>(t);
        }
        
        template<class T> 
        inline T&& forward(typename remove_reference<T>::type&& t)
        {
        return static_cast<T&&>(t);
        }
        
        template <class Fp, class A0, class ...Args>
        inline auto invoke(Fp && f, A0 && a0, Args && ...args)
        -> decltype((forward<A0>(a0).*f)(forward<Args>(args)...))
        {
        return (forward<A0>(a0).*f)(forward<Args>(args)...);
        }
        
        template <class Fp, class A0, class ...Args>
        inline auto invoke(Fp && f, A0 && a0, Args && ...args)
        -> decltype(((*forward<A0>(a0)).*f)(forward<Args>(args)...))
        {
           return ((*forward(a0)).*f)(forward(args)...);
        }
        
      • 从Range-v3当前工作中简化的测试用例:

        int f(int *);
        
        namespace N {
           template<typename T> T val();
        
        template<typename T> using void_t = void;
        
            template<typename T, typename = void> struct trait {};
            template<typename T> struct trait<T, void_t<decltype(f(val<T>()))>> {
               typedef decltype(f(val<T>())) type;
           };
        }
        
        N::trait<int *>::type t1;
        
        struct S {
          template<typename T> static T val();
        
          template<typename T> using void_t = void;
        
        template<typename T, typename = void> struct trait {};
            template<typename T> struct trait<T, void_t<decltype(f(val<T>()))>> {
               typedef decltype(f(val<T>())) type;
           };
        };
        
        S::trait<int *>::type t2;
        
      • 还有这个例子:

        int g;
        
        template<typename T>
        using void_t = void;
        
        template<typename T, typename = void>
        struct S1 {};
        
        template<typename T>
        struct S1<T, void_t<decltype(g + T{}) >> {};
        
        struct S2 {
        int *g;
        auto f() -> decltype(S1<int>());
        };
        

 

还需要做哪些工作?

        • 在模板参数中使用constexpr
        • 无论什么时候,当我们看到一个函数调用在模板中的no-type 参数的时候,我们就会立即对其进行语法分析和词法分析。这样会避免它在一个依赖环境中使用。在range-v3中constexpr通常广泛地被作为non-type模板参数来使用。

 

            #include <type_traits>
     
        #if defined(WORKAROUND1)
        // Alternative for type_trait.
      template<typename T, std::enable_if_t<std::is_pointer<T>::value> * = nullptr>
     #elif defined(WORKAROUND2)
      // If the constexpr is always valid, it can be moved into a helper class
        template<typename T>
      struct helper {
         static const bool value = T{};
      };
      template<typename T, std::enable_if_t<helper<std::is_pointer<T>>::value> * = nullptr>
       #else
       // VC does semantic analysis on 'std::is_pointer<T>{}' prematurely
        // and gives error on the function declaration
      //   error C2512: 'std::is_pointer<_Ty>': no appropriate default constructor available
        template<typename T, std::enable_if_t<std::is_pointer<T>{}> * = nullptr>
      #endif
      short f(const T &);
     
        char f(...);
        
        int main()
      {
           int *p = nullptr;
       
            static_assert(sizeof(f(0)) == sizeof(char), "fail");
            static_assert(sizeof(f(p)) == sizeof(short), "fail");
                }

 

  •  模板别名中的Decltype和dependent expressions:如果模板别名的定义包含decltype 或者dependent expressions,我们将会捕获它的标记并且随后重新解析它。因为标记不解析名称(它们只是存储标识符),有时候在重新解析时别名特化会发现错误的符号并且你可能会得到不正确的SFINAE结果。比如,下面有两段代码:

     // General case
    template<typename> struct S {
        using type = int;
    };
    template<typename T> using type1 = decltype(S<T>{});
    template<typename T> using type2 = typename type1<T>::type;
    type2<int> i;
    
     // This case impacts SFINAE. Simplified from range-v3
    
    template<typename> struct S1 {};
    
    template<typename T> T declval();
    
    void f(int *);
    
    #ifdef WORKAROUND
    template <typename T>
    using void_t = void;
    template <class T, class V = void> struct helper {};
    template <class T> struct helper<T, void_t<decltype(f(declval<T>()))>> {
       typedef decltype(f(declval<T>())) type;
    };
    template<typename T>
    using S2 = typename helper<T>::type;
    #else
    template<typename T>
    using S2 = decltype(f(declval<T>()));
    #endif
    
    template<typename U, typename T, S1<S2<U>> * = nullptr>
    short g(U, T);
    
    char g(...);
    
    void h()
    {
      // we are supposed to use 'std::nullptr_t' to specialize S2<U>, 
             // but we incorrectly use 'int' during re-parsing
       static_assert(sizeof(g(nullptr, 1)) == sizeof(short), "fail");
       // we are supposed to use 'int' to specialize S2<U>, 
             // but we incorrectly use 'std::nullptr_t' during re-parsing
       static_assert(sizeof(g(1, nullptr)) == sizeof(char), "fail");
    }
    
  • Decltype表达式中使用pack expansions: 我们目前并不能检测在decltype中的pack expansions,所以在某些情况下它不能正确地被扩展。你有可能会因此得到错误的SFINAE结果。

     // Example from range-v3:
    
    #ifdef WORKAROUND
    template<typename T>
    struct helper1 {};
    template<typename Fun2, typename ...Args2>
    struct helper2 {
       // pack expansion works for function template
       template<typename Fun, typename ...Args>
      static short f(helper1<decltype(val<Fun>()(val<Args>()...))> * = 0);
      template<typename Fun, typename ...Args>
      static char f(...);
    
     static const bool value 
                = sizeof(helper2::f<Fun2, Args2...>(0)) == sizeof(short);
    };
    
    template<bool, typename Fun, typename ...Args>
    struct helper {
    };
    template<typename Fun, typename ...Args>
    struct helper<true, Fun, Args...> {
       typedef decltype(val<Fun>()(val<Args>()...)) type;
    };
    
    template<typename Fun, typename ...Args>
    using result_t = typename helper<helper2<Fun, Args...>::value, Fun, Args...>::type;
    #else
    template<typename Fun, typename ...Args>
    using result_t = decltype(val<Fun>()(val<Args>()...));
    #endif
    

 

发送你们的反馈!

像往常一样,我们希望听到你们的反馈。如果你知道任何这篇博客中没有提及的关于SFINAE表达式的问题,请给我们发送你的反馈。你可以通过下面的评论,链接,或者给visualcpp@microsoft.com写邮件来发送。非常感谢!

 

今天就去试一下新的编译器吧! 关于VS 2015 Update 3RC 发布版的官方通告在这里:https://blogs.msdn.microsoft.com/visualstudio/2016/06/07/visual-studio-2015-update-3-rc/