C++11 SFINAE概念介绍:类成员的编译时内省(译)

An introduction to C++'s SFINAE concept: compile-time introspection of a class member

C++的SFINAE概念介绍:类成员的编译时内省

Trivia:

As a C++ enthusiast, I usually follow the annual C++ conference cppconf or at least try to keep myself up-to-date with the major events that happen there. One way to catch up, if you can’t afford a plane ticket or the ticket, is to follow the youtube channel dedicated to this conference. This year, I was impressed by Louis Dionne talk entitled “C++ Metaprogramming: A Paradigm Shift”. One feature called is_valid that can be found in Louis’s Boost.Hana library particulary caught my attention. This genious is_valid function heavily rely on an even more “magic” C++ programming technique coined with the term SFINAE discovered at the end of the previous century. If this acronym doesn’t speak to you, don’t be scared, we are going to dive straight in the subject.

Note: for the sake of your sanity and the fact that errare humanum est, this article might not be 100% accurate!

  • 作为一个 C++ 爱好者,我经常关注 C++ 年度会议 cppconf 或者至少试着让自己跟上发生在那里的重大事件。如果无法负担飞机票,一种赶上的方法是关注 youtube 专门为会议开设的频道。今年,我对 Louis Dionne 名为 “C++ Metaprogramming: A Paradigm Shift” 的演讲印象深刻。特别是在 Louis 的 Boost.Hana 库中一个名为 is_valid 的功能引起了我的注意。这个天才的 is_valid 功能严重依赖于一个更为“神奇的” C++ 编程技术,这种技术是在上世纪末用 SFINAE 术语创造的。如果这个缩略词你很陌生,不用害怕,我们将直接深入这个主题。
Introspection in C++?

C++ 中的内省

Before explaining what is SFINAE, let’s explore one of its main usage: introspection. As you might be aware, C++ doesn’t excel when it comes to examine the type or properties of an object at runtime. The best ability provided by default would be RTTI. Not only RTTI isn’t always available, but it also gives you barely more than the current type of the manipulated object. Dynamic languages or those having reflection on the other hand are really convenient in some situations like serialization.

For instance, in Python, using reflection, one can do the following:

  • 在解释什么是 SFINAE 之前,让我们先探讨它的主要用法之一:自省。你可能知道,C++ 在运行时检查对象的类型或者属性并不擅长。默认情况下提供的最佳功能是 RTTI。RTTI 不仅不是总是可用,而且它仅仅提供了当前被操作对象的类型。另一方面,动态语言或具有反射机制的语言在序列化等场景下很方便。
  • 例如,在Python中,使用反射,可以执行以下操作:
    class A(object):
        # Simply overrides the 'object.__str__' method.
        def __str__(self):
            return "I am a A"
    
    class B(object):
        # A custom method for my custom objects that I want to serialize.
        def serialize(self):
            return "I am a B"
    
    class C(object):
        def __init__(self):
            # Oups! 'serialize' is not a method. 
            self.serialize = 0
    
        def __str__(self):
            return "I am a C"
    
    def serialize(obj):
        # Let's check if obj has an attribute called 'serialize'.
        if hasattr(obj, "serialize"):
            # Let's check if this 'serialize' attribute is a method.
            if hasattr(obj.serialize, "__call__"):
                return obj.serialize()
    
        # Else we call the __str__ method.
        return str(obj)
    
    a = A()
    b = B()
    c = C()
    
    print(serialize(a)) # output: I am a A.
    print(serialize(b)) # output: I am a B.
    print(serialize(c)) # output: I am a C.
    

As you can see, during serialization, it comes pretty handy to be able to check if an object has an attribute and to query the type of this attribute. In our case, it permits us to use the serialize method if available and fall back to the more generic method str otherwise. Powerful, isn’t it? Well, we can do it in plain C++!

Here is the C++14 solution mentionned in Boost.Hana documentation, using is_valid:

  • 正如你所看到的,在序列化期间,能够检查对象是否具有属性和查询该属性的类型非常方便。在我们的例子中,它允许我们在可用的情况下使用 serialize 方法,否则返回到更通用的方法 str。很强大,是吗? 嗯,我们可以用 纯 C++ 做到!
  • 这是在 Boost.Hana 文档中提到的C++ 14解决方案,使用 is_valid:
    #include <boost/hana.hpp>
    #include <iostream>
    #include <string>
    
    using namespace std;
    namespace hana = boost::hana;
    
    // Check if a type has a serialize method.
    auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });
    
    // Serialize any kind of objects.
    template <typename T>
    std::string serialize(T const& obj) {
        return hana::if_(hasSerialize(obj), // Serialize is selected if available!
                         [](auto& x) { return x.serialize(); },
                         [](auto& x) { return to_string(x); }
        )(obj);
    }
    
    // Type A with only a to_string overload.
    struct A {};
    
    std::string to_string(const A&)
    {
        return "I am a A!";
    }
    
    // Type B with a serialize method.
    struct B
    {
        std::string serialize() const
        {
            return "I am a B!";
        }
    };
    
    // Type C with a "wrong" serialize member (not a method) and a to_string overload.
    struct C
    {
        std::string serialize;
    };
    
    std::string to_string(const C&)
    {
        return "I am a C!";
    }
    
    int main() {
        A a;
        B b;
        C c;
    
        std::cout << serialize(a) << std::endl;
        std::cout << serialize(b) << std::endl;
        std::cout << serialize(c) << std::endl;
    }
    

As you can see, it only requires a bit more of boilerplate than Python, but not as much as you would expect from a language as complexe as C++. How does it work? Well if you are too lazy to read the rest, here is the simplest answer I can give you: unlike dynamically typed languages, your compiler has access a lot of static type information once fired. It makes sense that we can constraint your compiler to do a bit of work on these types! The next question that comes to your mind is “How to?”. Well, right below we are going to explore the various options we have to enslave our favorite compiler for fun and profit! And we will eventually recreate our own is_valid.

  • 正如你所看到的,它只需要比Python多一些样板,但是没有你所期望的像c++这样复杂的语言所需要的那么多。它是如何工作的?如果你懒得读剩下的内容,下面是我能给你的最简单的答案:与动态类型语言不同,编译器在启动后可以访问大量的静态类型信息。我们可以约束编译器对这些类型做一些工作,这是有意义的!下一个你想到的问题是“怎么做?”。好吧,下面我们将探讨各种各样的选择,我们必须奴役我们最喜爱的编译器,以获得乐趣和收益!我们最终会重新创造我们自己的 is_valid。
The old-fashioned C++98-way:

老式的 C++ 98 方法:

Whether your compiler is a dinosaur, your boss refuses to pay for the latest Visual Studio license or you simply love archeology, this chapter will interest you. It’s also interesting for the people stuck between C++ 11 and C++ 14. The solution in C++98 relies on 3 key concepts: overload resolution, SFINAE and the static behavior of sizeof.

  • 无论你的编译器是恐龙级(过时的),你的老板拒绝支付最新的 Visual Studio 许可证,还是你因为喜欢考古学,这一章都会让你感兴趣。对于那些困在 c++ 11 和 c++ 14 之间的人来说,这也很有趣。c++ 98 中的解决方案依赖于3个关键概念:重载决议、SFINAE 和 sizeof 的静态行为。
Overload resolution:

A simple function call like “f(obj);”" in C++ activates a mechanism to figure out which f function shoud be called according to the argument obj. If a set of f functions could accept obj as an argument, the compiler must choose the most appropriate function, or in other words resolve the best overload! Here is a good cppreference page explaining the full process: Overload resolution. The rule of thumb in this case is the compiler picks the candidate function whose parameters match the arguments most closely is the one that is called. Nothing is better than a good example:

  • 一个简单的函数调用比如 “f(obj);” 在 c++ 中激活一种机制,根据参数 obj 来找出哪个函数应该被调用。如果一组 f 函数可以接受 obj 作为参数,编译器必须选择最合适的函数,或者换句话说,决定最好的重载!这里有一个很好的 cppreference 页面解释了整个过程: 重载决议。在这种情况下,经验法则是,编译器选择其形参类型最匹配实参的候选函数进行调用。举例如下:
    void f(std::string s); // int can't be convert into a string.
    void f(double d); // int can be implicitly convert into a double, so this version could be selected, but...
    void f(int i); // ... this version using the type int directly is even more close!
    
    f(1); // Call f(int i);
    

In C++ you also have some sink-hole functions that accept everything. First, function templates accept any kind of parameter (let’s say T). But the true black-hole of your compiler, the devil variable vacuum, the oblivion of the forgotten types are the variadic functions. Yes, exactly like the horrible C printf.

  • 在C++中,你也有一些接收一切东西的陷洞函数(sink-hole functions)。首先,函数模板接受任何类型的参数(比如 T)。但是编译器真正的黑洞是可变参数函数。是的,就像可怕的 C printf。
    std::string f(...); // Variadic functions are so "untyped" that...
    template <typename T> std::string f(const T& t); // ...this templated function got the precedence!
    
    f(1); // Call the templated function version of f.
    

The fact that function templates are less generic than variadic functions is the first point you must remember!

Note: A templated function can actually be more precise than a normal function. However, in case of a draw, the normal function will have the precedence.

  • 函数模板不如可变函数通用,这是你必须记住的第一点!
  • 注意:模板化的函数实际上比普通函数更精确。然而,在平局时,普通函数优先级更高。
SFINAE:

I am already teasing you with the power for already few paragraphs and here finally comes the explanation of this not so complex acronym. SFINAE stands for Substitution Failure Is Not An Error. In rough terms, a substitution is the mechanism that tries to replace the template parameters with the provided types or values. In some cases, if the substitution leads to an invalid code, the compiler shouldn’t throw a massive amount of errors but simply continue to try the other available overloads. The SFINAE concept simply guaranties such a “sane” behavior for a “sane” compiler. For instance:

  • SFINAE 是 Substitution Failure Is Not An Error 的缩写(替换失败不是错误)。粗略地说,替换是尝试用所提供的类型或值替换模板参数的机制。在某些情况下,如果替换导致无效代码,编译器不应该抛出大量错误,而应该继续尝试其他可用的重载。SFINAE概念简单地保证了“健全的”编译器的这种“健全的”行为。例如:
    /*
     The compiler will try this overload since it's less generic than the variadic.
     T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr);
     int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors.
     It simply tries the next overload. 
    */
    template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }
    
    // The sink-hole.
    void f(...) { }
    
    f(1); // Calls void f(...) { }
    

All the expressions won’t lead to a SFINAE. A broad rule would be to say that all the substitutions out of the function/methods body are “safes”. For a better list, please take a look at this wiki page. For instance, a wrong substitution within a function body will lead to a horrible C++ template error:

  • 所有的表达式都不会导致SFINAE。一个宽泛的规则是,函数/方法主体之外的所有替换都是“安全的”。更多请查看这个wiki页面(是的,原文链接如此)。例如,函数体内的错误替换会导致可怕的 c++ 模板错误:
    // The compiler will be really unhappy when it will later discover the call to hahahaICrash. 
    template <typename T> void f(T t) { t.hahahaICrash(); }
    void f(...) { } // The sink-hole wasn't even considered.
    
    f(1);
    
The operator sizeof:

The sizeof operator is really a nice tool! It permits us to returns the size in bytes of a type or an expression at compilation time. sizeof is really interesting as it accurately evaluates an expression as precisely as if it were compiled. One can for instance do:

  • sizeof操作符确实是一个很好的工具!它允许我们在编译时以字节为单位返回类型或表达式的大小。sizeof 精确地计算表达式,就像编译编译表达式一样精确。我们可以这样做:
    typedef char type_test[42];
    type_test& f();
    
    // In the following lines f won't even be truly called but we can still access to the size of its return type.
    // Thanks to the "fake evaluation" of the sizeof operator.
    char arrayTest[sizeof(f())];
    std::cout << sizeof(f()) << std::endl; // Output 42.
    

But wait! If we can manipulate some compile-time integers, couldn’t we do some compile-time comparison? The answer is: absolutely yes, my dear reader! Here we are:

  • 但是等等!如果我们可以操作一些编译时整数,我们就不能做一些编译时比较吗?答案是:绝对可以。在这里,我们有:
    typedef char yes; // Size: 1 byte.
    typedef yes no[2]; // Size: 2 bytes.
    
    // Two functions using our type with different size.
    yes& f1();
    no& f2();
    
    std::cout << (sizeof(f1()) == sizeof(f2())) << std::endl; // Output 0.
    std::cout << (sizeof(f1()) == sizeof(f1())) << std::endl; // Output 1.
    
Combining everything:

Now we have all the tools to create a solution to check the existence of a method within a type at compile time. You might even have already figured it out most of it by yourself. So let’s create it:

  • 现在我们有了创建解决方案的所有工具,可以在编译时检查类型中是否存在方法。你可能已经自己解决了大部分问题。让我们创建它:
    template <class T> struct hasSerialize
    {
        // For the compile time comparison.
        typedef char yes[1];
        typedef yes no[2];
    
        // This helper struct permits us to check that serialize is truly a method.
        // The second argument must be of the type of the first.
        // For instance reallyHas<int, 10> would be substituted by reallyHas<int, int 10> and works!
        // reallyHas<int, &C::serialize> would be substituted by reallyHas<int, int &C::serialize> and fail!
        // Note: It only works with integral constants and pointers (so function pointers work).
        // In our case we check that &C::serialize has the same signature as the first argument!
        // reallyHas<std::string (C::*)(), &C::serialize> should be substituted by 
        // reallyHas<std::string (C::*)(), std::string (C::*)() &C::serialize> and work!
        template <typename U, U u> struct reallyHas;
    
        // Two overloads for yes: one for the signature of a normal method, one is for the signature of a const method.
        // We accept a pointer to our helper struct, in order to avoid to instantiate a real instance of this type.
        // std::string (C::*)() is function pointer declaration.
        template <typename C> static yes& test(reallyHas<std::string (C::*)(), &C::serialize>* /*unused*/) { }
        template <typename C> static yes& test(reallyHas<std::string (C::*)() const, &C::serialize>* /*unused*/) { }
    
        // The famous C++ sink-hole.
        // Note that sink-hole must be templated too as we are testing test<T>(0).
        // If the method serialize isn't available, we will end up in this method.
        template <typename> static no& test(...) { /* dark matter */ }
    
        // The constant used as a return value for the test.
        // The test is actually done here, thanks to the sizeof compile-time evaluation.
        static const bool value = sizeof(test<T>(0)) == sizeof(yes);
    };
    
    // Using the struct A, B, C defined in the previous hasSerialize example.
    std::cout << hasSerialize<A>::value << std::endl;
    std::cout << hasSerialize<B>::value << std::endl;
    std::cout << hasSerialize<C>::value << std::endl;
    

The reallyHas struct is kinda tricky but necessary to ensure that serialize is a method and not a simple member of the type. You can do a lot of test on a type using variants of this solution (test a member, a sub-type…) and I suggest you to google a bit more about SFINAE tricks. Note: if you truly want a pure compile-time constant and avoid some errors on old compilers, you can replace the last value evaluation by: “enum { value = sizeof(test(0)) == sizeof(yes) };”.

You might also wonder why it doesn’t work with inheritence. Inheritence in C++ and dynamic polymorphism is a concept available at runtime, or in other words, a data that the compiler won’t have and can’t guess! However, compile time type inspection is much more efficient (0 impact at runtime) and almost as powerful as if it were at runtime. For instance:

  • reallyHas 结构有几分巧妙但很必要来确保serialize是一个方法,而不是简单的类型成员。您可以使用此解决方案的变体对类型进行大量测试(测试成员、子类型……),我建议你在谷歌中多了解一些 SFINAE 技巧。注意:如果你真的想要一个纯粹的编译时常量并且避免旧编译器上的一些错误,你可以用:"enum {value = sizeof(test(0)) == sizeof(yes)};"代替最后的值计算。

  • 你可能还想知道为什么它不能与继承一起工作。c++ 中的继承和动态多态是一个在运行时可用的概念,或者换句话说,是编译器不会拥有也无法猜测的数据!但是,编译时类型检查效率更高(在运行时影响为0),而且几乎和在运行时一样强大。例如:

    // Using the previous A struct and hasSerialize helper.
    
    struct D : A
    {
        std::string serialize() const
        {
            return "I am a D!";
        }
    };
    
    template <class T> bool testHasSerialize(const T& /*t*/) { return hasSerialize<T>::value; }
    
    D d;
    A& a = d; // Here we lost the type of d at compile time.
    std::cout << testHasSerialize(d) << std::endl; // Output 1.
    std::cout << testHasSerialize(a) << std::endl; // Output 0.
    

Last but no least, our test cover the main cases but not the tricky ones like a Functor:

  • 最后但并非最不重要的是,我们的测试涵盖了主要的情况,但没有棘手的情况例如 Functor:
    struct E
    {
        struct Functor
        {
            std::string operator()()
            {
                return "I am a E!";
            }
        };
    
        Functor serialize;
    };
    
    E e;
    std::cout << e.serialize() << std::endl; // Succefully call the functor.
    std::cout << testHasSerialize(e) << std::endl; // Output 0.
    

The trade-off for a full coverage would be the readability. As you will see, C++ 11 shines in that domain!

  • 全覆盖的代价是可读性。正如你将看到的,c++ 11在这个领域非常出色!
Time to use our genius idea:

Now you would think that it will be super easy to use our hasSerialize to create a serialize function! Okay let’s try it:

  • 现在你可能认为使用 hasSerialize 创建序列化函数会非常容易!好吧,我们来试试:
    template <class T> std::string serialize(const T& obj)
    {
        if (hasSerialize<T>::value) {
            return obj.serialize(); // error: no member named 'serialize' in 'A'.
        } else {
            return to_string(obj);
        }
    }
    
    A a;
    serialize(a);
    

It might be hard to accept, but the error raised by your compiler is absolutely normal! If you consider the code that you will obtain after substitution and compile-time evaluation:

  • 这可能很难接受,但是编译器引发的错误是绝对正常的!如果你考虑在替换和编译时计算后获得的代码:
    std::string serialize(const A& obj)
    {
        if (0) { // Dead branching, but the compiler will still consider it!
            return obj.serialize(); // error: no member named 'serialize' in 'A'.
        } else {
            return to_string(obj);
        }
    }
    

Your compiler is really a good guy and won’t drop any dead-branch, and obj must therefore have both a serialize method and a to_string overload in this case. The solution consists in spliting the serialize function into two different functions: one where we solely use obj.serialize() and one where we use to_string according to obj’s type. We come back to an earlier problem that we already solved, how to split according to a type? SFINAE, for sure! At that point we could re-work our hasSerialize function into a serialize function and make it return a std::string instead of compile time boolean. But we won’t do it that way! It’s cleaner to separate the hasSerialize test from its usage serialize.

We need to find a clever SFINAE solution on the signature of “template std::string serialize(const T& obj)”. I bring you the last piece of the puzzle called enable_if.

  • 你的编译器真的很好,不会丢弃任何死分支,因此 obj 必须同时拥有一个 serialize 方法和一个 to_string 重载。解决方案是将 serialize 函数拆分为两个不同的函数:一个是我们单独使用 obj.serialize(),另一个是我们根据 obj 的类型使用 to_string。我们回到之前已经解决的问题,如何根据类型划分? SFINAE,肯定的!此时,我们可以将 hasSerialize 函数改写为 serialize 函数,并使其返回 std::string 而不是编译时布尔值。但我们不会那样做!将 hasSerialize 测试与它的使用分开会更清晰。

  • 我们需要在签名 “template std::string serialize(const T& obj)” 上找到一个聪明的 SFINAE 解决方案。我将向你介绍这个谜题的最后一块称为 enable_if 的内容。

    template<bool B, class T = void> // Default template version.
    struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.
    
    template<class T> // A specialisation used if the expression is true. 
    struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.
    
    // Usage:
    enable_if<true, int>::type t1; // Compiler happy. t's type is int.
    enable_if<hasSerialize<B>::value, int>::type t2; // Compiler happy. t's type is int.
    
    enable_if<false, int>::type t3; // Compiler unhappy. no type named 'type' in 'enable_if<false, int>';
    enable_if<hasSerialize<A>::value, int>::type t4; // no type named 'type' in 'enable_if<false, int>';
    

As you can see, we can trigger a substitution failure according to a compile time expression with enable_if. Now we can use this failure on the “template std::string serialize(const T& obj)” signature to dispatch to the right version. Finally, we have the true solution of our problem:

  • 如你所见,我们可以使用 enable_if 根据编译时表达式触发替换失败。现在我们可以在 “template std::string serialize(const T& obj)” 签名上使用这个失败来分发到正确的版本。最后,我们有了解决问题的真正办法:
    template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj)
    {
        return obj.serialize();
    }
    
    template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj)
    {
        return to_string(obj);
    }
    
    A a;
    B b;
    C c;
    
    // The following lines work like a charm!
    std::cout << serialize(a) << std::endl;
    std::cout << serialize(b) << std::endl;
    std::cout << serialize(c) << std::endl;
    

Two details worth being noted! Firstly we use enable_if on the return type, in order to keep the paramater deduction, otherwise we would have to specify the type explicitely “serialize(a)”. Second, even the version using to_string must use the enable_if, otherwise serialize(b) would have two potential overloads available and raise an ambiguity. If you want to check the full code of this C++ 98 version, here is a gist. Life is much easier in C++ 11, so let’s see the beauty of this new standard!

Note: it’s also important to know that this code creates a SFINAE on an expression ("&C::serialize"). Whilst this feature wasn’t required by the C++ 98 standard, it was already in use depending on your compiler. It trully became a safe choice in C++ 11.

  • 有两个细节值得注意!首先,我们在返回类型上使用 enable_if,以保持参数推导,否则我们将不得不明确地指定类型 “serialize(a)”。其次,即使是使用 to_string 的版本也必须使用 enable_if ,否则 serialize(b) 将有两个潜在的重载可用,并导致歧义。如果你想检查这个c++ 98版本的完整代码,这里是要点(链接打不开)。在c++ 11中要简单得多,所以让我们来看看这个新标准的美吧!
  • 注意:同样重要的是,要知道此代码在表达式 ("&C::serialize") 上创建了SFINAE。虽然这个特性不是c++ 98标准所要求的,但它已经在使用了,这取决于您的编译器。在c++ 11中,它真的成为了一个安全的选择。
When C++11 came to our help:

After the great century leap year in 2000, people were fairly optimistic about the coming years. Some even decided to design a new standard for the next generation of C++ coders like me! Not only this standard would ease TMP headaches (Template Meta Programming side-effects), but it would be available in the first decade, hence its code-name C++ 0x. Well, the standard sadly came the next decade (2011 ==> C++11), but it brought a lot of features interesting for the purpose of this article. Let’s review them!

  • 在2000年之后,人们对未来几年相当乐观。有些人甚至决定为像我这样的下一代 c++ 程序员设计一个新标准!这个标准不仅可以减轻TMP头疼(Template Meta Programming 模板元编程的副作用),而且在第一个十年就可以使用,因此它的代码名为c++ 0x。遗憾的是,标准是在接下来的十年(2011 ==> c++ 11)出现的,但是它带来了许多有趣的特性,这对于本文的目的来说很有趣。让我们回顾一下它们!
decltype, declval, auto & co:

Do you remember that the sizeof operator does a “fake evaluation” of the expression that you pass to it, and return gives you the size of the type of the expression? Well C++11 adds a new operator called decltype. decltype gives you the type of the of the expression it will evaluate. As I am kind, I won’t let you google an example and give it to you directly:

  • 你还记得 sizeof 操作符对传递给它的表达式执行“伪求值”,并返回表达式类型的大小吗? c++ 11添加了一个新的操作符 decltype。decltype 给出了它要计算的表达式的类型。例子如下:
    B b;
    decltype(b.serialize()) test = "test"; // Evaluate b.serialize(), which is typed as std::string.
    // Equivalent to std::string test = "test";
    

declval is an utility that gives you a “fake reference” to an object of a type that couldn’t be easily construct. declval is really handy for our SFINAE constructions. cppreference example is really straightforward, so here is a copy:

  • declval 是一个实用程序,它为你提供了一个对不容易构造的类型的对象的“假引用”。declval对于我们的 SFINAE 构造来说真的很方便。cppreference 示例非常简单,如下:
    struct Default {
        int foo() const {return 1;}
    };
    
    struct NonDefault {
        NonDefault(const NonDefault&) {}
        int foo() const {return 1;}
    };
    
    int main()
    {
        decltype(Default().foo()) n1 = 1; // int n1
    //  decltype(NonDefault().foo()) n2 = n1; // error: no default constructor
        decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2
        std::cout << "n2 = " << n2 << '\n';
    }
    

The auto specifier specifies that the type of the variable that is being declared will be automatically deduced. auto is equivalent of var in C#. auto in C++11 has also a less famous but nonetheless usage for function declaration. Here is a good example:

  • auto 说明符指定将自动推导所声明的变量的类型。auto 在 c# 中相当于 var。c++ 11中的 auto 还有一个不太有名的用法用于函数声明。这里有一个很好的例子:
    bool f();
    auto test = f(); // Famous usage, auto deduced that test is a boolean, hurray!
    
    
    // t wasn't declare at that point, it will be after as a parameter!
    template <typename T> decltype(t.serialize()) g(const T& t) {   } // Compilation error
    
    // Less famous usage:
    //    auto delayed the return type specification!
    //    the return type is specified here and use t!
    template <typename T> auto g(const T& t) -> decltype(t.serialize()) {   } // No compilation error.
    

As you can see, auto permits to use the trailing return type syntax and use decltype coupled with an expression involving one of the function argument. Does it means that we can use it to test the existence of serialize with a SFINAE? Yes Dr. Watson! decltype will shine really soon, you will have to wait for the C++ 14 for this tricky auto usage (but since it’s a C++ 11 feature, it ends up here).

  • 如你所见,auto允许使用尾部返回类型语法,并将 decltype 与包含函数参数之一的表达式结合使用。这是否意味着我们可以使用它来测试是否存在 SFINAE 序列化?是的。
constexpr:

C++ 11 also came with a new way to do compile-time computations! The new keyword constexpr is a hint for your compiler, meaning that this expression is constant and could be evaluate directly at compile time. In C++11, constexpr has a lot of rules and only a small subset of VIEs (Very Important Expression) expressions can be used (no loops…)! We still have enough for creating a compile-time factorial function:

  • c++ 11还提供了一种新的编译时计算方法!新的关键字 constexpr 是给编译器的一个提示,这意味着这个表达式是常量,可以在编译时直接求值。在c++ 11中,constexpr 有很多规则,只有一小部分VIEs(Very Important Expression 非常重要的表达式)表达式可以使用(没有循环…)!我们仍然足够创建编译时阶乘函数:
    constexpr int factorial(int n)
    {
        return n <= 1? 1 : (n * factorial(n - 1));
    }
    
    int i = factorial(5); // Call to a constexpr function.
    // Will be replace by a good compiler by:
    // int i = 120;
    

constexpr increased the usage of std::true_type & std::false_type from the STL. As their name suggest, these types encapsulate a constexpr boolean “true” and a constrexpr boolean “false”. Their most important property is that a class or a struct can inherit from them. For instance:

  • constexpr 增加了std::true_type 和 std::false_type 的使用。顾名思义,这些类型封装了一个 constexpr 布尔值“真”和一个 constrexpr 布尔值“假”。它们最重要的属性是类或结构可以继承它们。例如:
    struct testStruct : std::true_type { }; // Inherit from the true type.
    
    constexpr bool testVar = testStruct(); // Generate a compile-time testStruct.
    bool test = testStruct::value; // Equivalent to: test = true;
    test = testVar; // true_type has a constexpr converter operator, equivalent to: test = true;
    
Blending time:
First solution:

In cooking, a good recipe requires to mix all the best ingredients in the right proportions. If you don’t want to have a spaghetti code dating from 1998 for dinner, let’s revisit our C++98 hasSerialize and serialize functions with “fresh” ingredients from 2011. Let’s start by removing the rotting reallyHas trick with a tasty decltype and bake a bit of constexpr instead of sizeof. After 15min in the oven (or fighting with a new headache), you will obtain:

  • 烹饪时,一个好的食谱需要把所有最好的配料按正确的比例混合。如果您不想在晚餐时使用1998年的意大利面条式代码,那么让我们用2011年的“新鲜”原料重新访问 c++ 98 的 hasSerialize 和 serialize 函数。让我们用一个美味的 decltype 清除腐烂的 reallyHas ,用 constexpr 代替 sizeof。你会得到:
    template <class T> struct hasSerialize
    {
        // We test if the type has serialize using decltype and declval.
        template <typename C> static constexpr decltype(std::declval<C>().serialize(), bool()) test(int /* unused */)
        {
            // We can return values, thanks to constexpr instead of playing with sizeof.
            return true;
        }
    
        template <typename C> static constexpr bool test(...)
        {
            return false;
        }
    
        // int is used to give the precedence!
        static constexpr bool value = test<T>(int());
    };
    

You might be a bit puzzled by my usage of decltype. The C++ comma operator “,” can create a chain of multiple expressions. In decltype, all the expressions will be evaluated, but only the last expression will be considered for the type. The serialize doesn’t need any changes, minus the fact that the enable_if function is now provided in the STL. For your tests, here is a gist.

  • 你可能对我使用 decltype 有点困惑。c++ 逗号运算符","可以创建多个表达式链。在 decltype 中,所有的表达式都将被计算,但是只有最后一个表达式将被考虑为该类型。serialize 不需要进行任何更改,减去了现在STL中提供了 enable_if 函数的事实。
Second solution:

Another C++11 solution described in Boost.Hanna documentation and using std::true_type and std::false_type, would be this one:

  • Boost.Hanna 文档中描述的另一个c++ 11解决方案,使用std::true_type和std::false_type,将是这个:
    // Primary template, inherit from std::false_type.
    // ::value will return false. 
    // Note: the second unused template parameter is set to default as std::string!!!
    template <typename T, typename = std::string>
    struct hasSerialize
            : std::false_type
    {
    
    };
    
    // Partial template specialisation, inherit from std::true_type.
    // ::value will return true. 
    template <typename T>
    struct hasSerialize<T, decltype(std::declval<T>().serialize())>
            : std::true_type
    {
    
    };
    

This solution is, in my own opinion, more sneaky! It relies on a not-so-famous-property of default template parameters. But if your soul is already (stack-)corrupted, you may be aware that the default parameters are propagated in the specialisations. So when we use hasSerialize::value, the default parameter comes into play and we are actually looking for hasSerialize<OurType, std::string>::value both on the primary template and the specialisation. In the meantime, the substitution and the evaluation of decltype are processed and our specialisation has the signature hasSerialize<OurType, std::string> if OurType has a serialize method that returns a std::string, otherwise the substitution fails. The specialisation has therefore the precedence in the good cases. One will be able to use the std::void_t C++17 helper in these cases. Anyway, here is a gist you can play with!

I told you that this second solution hides a lot of complexity, and we still have a lot of C++ 11 features unexploited like nullptr, lambda, r-values. No worries, we are going to use some of them in C++14!

  • 在我看来,这种解决方法更狡猾!它依赖于默认模板参数这一不那么出名的属性。当我们使用 hasSerialize::value 时,默认参数就开始发挥作用了,我们实际上是在主模板和特化上寻找 hasSerialize<OurType, std::string>::value。同时,替换和decltype的求值被处理,如果 OurType 有一个返回 std::string 的 serialize 方法,我们的特化有签名hasSerialize<OurType, std::string>,否则替换失败。因此,特化在好的情况下优先级更高。在这些情况下,我们将能够使用std::void_t c++ 17 helper。
  • 我告诉过你,第二种解决方案隐藏了很多复杂性,我们还有很多c++ 11特性没有被利用,比如nullptr, lambda, r-value。不用担心,我们将在c++ 14中使用其中一些!
The supremacy of C++14:

According to the Gregorian calendar in the upper-right corner of my XFCE environment, we are in 2015! I can turn on the C++14 compilation flag on my favorite compiler safely, isn’t it? Well, I can with clang (is MSVC using a maya calendar?). Once again, let’s explore the new features, and use them to build something wonderful! We will even recreate an is_valid, like I promised at the beggining of this article.

auto & lambdas:
Return type inference:

Some cool features in C++14 come from the relaxed usage of the auto keyword (the one used for type inference).

Now, auto can be used on the return type of a function or a method. For instance:

  • c++ 14中一些很酷的特性来自于对auto关键字(用于类型推断的关键字)的轻松使用。
  • 现在,auto可以用于函数或方法的返回类型。例如:
    auto myFunction() // Automagically figures out that myFunction returns ints.
    {
        return int();
    }
    

It works as long as the type is easily “guessable” by the compiler. We are coding in C++ after all, not OCaml!

  • 只要类型能够被编译器很容易地“猜测”,它就可以工作。毕竟我们是用c++编写代码的,而不是 OCaml!
A feature for functional lovers:

C++ 11 introduced lambdas. A lambda has the following syntax:

[capture-list](params) -> non-mandatory-return-type { ...body... }

A useful example in our case would be:
```cpp
auto l1 = [](B& b) { return b.serialize(); }; // Return type figured-out by the return statement.
auto l3 = [](B& b) -> std::string { return b.serialize(); }; // Fixed return type.
auto l2 = [](B& b) -> decltype(b.serialize()) { return b.serialize(); }; // Return type dependant to the B type.

std::cout << l1(b) << std::endl; // Output: I am a B!
std::cout << l2(b) << std::endl; // Output: I am a B!
std::cout << l3(b) << std::endl; // Output: I am a B!
```

C++14 brings a small change to the lambdas but with a big impact! Lambdas accept auto parameters: the parameter type is deduced according the argument. Lambdas are implemented as an object having an newly created unnamed type, also called closure type. If a lambda has some auto parameters, its “Functor operator” operator() will be simply templated. Let’s take a look:

  • c++ 14 给 lambdas 带来了一个小的变化,但具有巨大的影响! Lambdas 接受 auto 参数:根据参数推断参数类型。Lambdas 被实现为具有新创建的未命名类型(也称为闭包类型)的对象。如果 lambda 有一些 auto 参数,它的“Functor 运算符” operator() 将被简单地模板化。让我们来看看:
    // ***** Simple lambda unamed type *****
    auto l4 = [](int a, int b) { return a + b; };
    std::cout << l4(4, 5) << std::endl; // Output 9.
    
    // Equivalent to:
    struct l4UnamedType
    {
        int operator()(int a, int b) const
        {
            return a + b;
        }
    };
    
    l4UnamedType l4Equivalent = l4UnamedType();
    std::cout << l4Equivalent(4, 5) << std::endl; // Output 9 too.
    
    
    
    // ***** auto parameters lambda unnamed type *****
    
    // b's type is automagically deduced!
    auto l5 = [](auto& t) -> decltype(t.serialize()) { return t.serialize(); };
    
    std::cout << l5(b) << std::endl; // Output: I am a B!
    std::cout << l5(a) << std::endl; // Error: no member named 'serialize' in 'A'.
    
    // Equivalent to:
    struct l5UnamedType
    {
        template <typename T> auto operator()(T& t) const -> decltype(t.serialize()) // /!\ This signature is nice for a SFINAE!
        {
            return t.serialize();
        }
    };
    
    l5UnamedType l5Equivalent = l5UnamedType();
    
    std::cout << l5Equivalent(b) << std::endl; // Output: I am a B!
    std::cout << l5Equivalent(a) << std::endl; // Error: no member named 'serialize' in 'A'.
    

More than the lambda itself, we are interested by the generated unnamed type: its lambda operator() can be used as a SFINAE! And as you can see, writing a lambda is less cumbersome than writing the equivalent type. It should remind you the beggining of my initial solution:

  • 除了 lambda 本身,我们还对生成的未命名类型感兴趣:它的lambda operator() 可以用作 SFINAE! 正如你所看到的,编写lambda要比编写等价的类型简单得多。它应该提醒你开始我最初的解决方案:
    // Check if a type has a serialize method.
    auto hasSerialize = hana::is_valid([](auto&& x) -> decltype(x.serialize()) { });
    

And the good new is that we have everything to recreate is_valid, right now!

  • 好消息是,我们有重建 is_valid 的一切东西,现在!
The making-of a valid is_valid:

Now that we have a really stylish manner to generate a unnamed types with potential SFINAE properties using lambdas, we need to figure out how to use them! As you can see, hana::is_valid is a function that takes our lambda as a parameter and return a type. We will call the type returned by is_valid the container. The container will be in charge to keep the lambda’s unnamed type for a later usage. Let’s start by writing the is_valid function and its the containter:

  • 现在我们有了一种真正时髦的方式来使用 lambdas 生成具有潜在SFINAE属性的未命名类型,我们需要弄清楚如何使用它们!如你所见,hana::is_valid 是一个函数,它将lambda作为参数并返回类型。我们将把 is_valid 返回的类型称为容器。容器将负责保留 lambda 的未命名类型以供以后使用。让我们从编写 is_valid 函数和它的容器开始:
    template <typename UnnamedType> struct container
    {
        // Remembers UnnamedType.
    };
    
    template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t) 
    {
        // We used auto for the return type: it will be deduced here.
        return container<UnnamedType>();
    }
    
    auto test = is_valid([](const auto& t) -> decltype(t.serialize()) {})
    // Now 'test' remembers the type of the lambda and the signature of its operator()!
    

The next step consists at extending container with the operator operator() such as we can call it with an argument. This argument type will be tested against the UnnamedType! In order to do a test on the argument type, we can use once again a SFINAE on a reacreated ‘UnnamedType’ object! It gives us this solution:

  • 下一步是用操作符 operator() 扩展容器,这样就可以用参数调用它。此参数类型将根据 UnnamedType 进行测试!为了对参数类型进行测试,我们可以再次对重新分配的’UnnamedType’对象使用SFINAE !它给出了这个解:
    template <typename UnnamedType> struct container
    {
    // Let's put the test in private.
    private:
        // We use std::declval to 'recreate' an object of 'UnnamedType'.
        // We use std::declval to also 'recreate' an object of type 'Param'.
        // We can use both of these recreated objects to test the validity!
        template <typename Param> constexpr auto testValidity(int /* unused */)
        -> decltype(std::declval<UnnamedType>()(std::declval<Param>()), std::true_type())
        {
            // If substitution didn't fail, we can return a true_type.
            return std::true_type();
        }
    
        template <typename Param> constexpr std::false_type testValidity(...)
        {
            // Our sink-hole returns a false_type.
            return std::false_type();
        }
    
    public:
        // A public operator() that accept the argument we wish to test onto the UnnamedType.
        // Notice that the return type is automatic!
        template <typename Param> constexpr auto operator()(const Param& p)
        {
            // The argument is forwarded to one of the two overloads.
            // The SFINAE on the 'true_type' will come into play to dispatch.
            // Once again, we use the int for the precedence.
            return testValidity<Param>(int());
        }
    };
    
    template <typename UnnamedType> constexpr auto is_valid(const UnnamedType& t) 
    {
        // We used auto for the return type: it will be deduced here.
        return container<UnnamedType>();
    }
    
    // Check if a type has a serialize method.
    auto hasSerialize = is_valid([](auto&& x) -> decltype(x.serialize()) { });
    

If you are a bit lost at that point, I suggest you take your time and re-read all the previous example. You have all the weapons you need, now fight C++!

Our hasSerialize now takes an argument, we therefore need some changes for our serialize function. We can simply post-pone the return type using auto and use the argument in a decltype as we learn. Which gives us:

  • 我们的 hasSerialize 现在带有一个参数,因此我们需要对 serialize 函数进行一些更改。我们可以简单地使用 auto 延迟返回类型以及在decltype中使用参数,如下:
    // Notice how I simply swapped the return type on the right?
    template <class T> auto serialize(T& obj) 
    -> typename std::enable_if<decltype(hasSerialize(obj))::value, std::string>::type
    {
        return obj.serialize();
    }
    
    template <class T> auto serialize(T& obj) 
    -> typename std::enable_if<!decltype(hasSerialize(obj))::value, std::string>::type
    {
        return to_string(obj);
    }
    

FINALLY!!! We do have a working is_valid and we could use it for serialization! If I were as vicious as my SFINAE tricks, I would let you copy each code pieces to recreate a fully working solution. But today, Halloween’s spirit is with me and here is gist. Hey, hey! Don’t close this article so fast! If you are true a warrior, you can read the last part!

For the fun:

There are few things I didn’t tell you, on purpose. This article would otherwise be twice longer, I fear. I highly suggest you to google a bit more about what I am going to speak about.

Firstly, if you wish to have a solution that works with the Boost.Hana static if_, you need to change the return type of our testValidity methods by Hana’s equivalents, like the following:

  • 首先,如果你希望有一个与 Boost.Hana static if_ 一起工作的解决方案。静态if_,你需要改变我们的 testValidity 方法的返回类型为 Hana 的等效物,如下所示:
    template <typename Param> constexpr auto test_validity(int /* unused */)
    -> decltype(std::declval<UnnamedType>()(std::declval<Param>()), boost::hana::true_c)
    {
        // If substitution didn't fail, we can return a true_type.
        return boost::hana::true_c;
    }
    
    template <typename Param> constexpr decltype(boost::hana::false_c) test_validity(...)
    {
        // Our sink-hole returns a false_type.
        return boost::hana::false_c;
    }
    

The static if_ implementation is really interesting, but at least as hard as our is_valid problem solved in this article. I might dedicate another article about it, one day!

Did you noticed that we only check one argument at a time? Couldn’t we do something like:

  • static if_ 实现确实很有趣,但至少和本文中解决的 is_valid 问题一样困难。也许有一天我会再写一篇关于它的文章!

  • 你注意到我们一次只检查一个参数了吗?我们能不能这样做:

    auto test = is_valid([](auto&& a, auto&& b) -> decltype(a.serialize(), b.serialize()) { });
    A a;
    B b;
    
    std::cout << test(a, b) << std::endl;
    

Actually we can, using some parameter packs. Here is the solution:

  • 实际上我们可以使用一些参数包。下面是解决方案:
    template <typename UnnamedType> struct container
    {
    // Let's put the test in private.
    private:
        // We use std::declval to 'recreate' an object of 'UnnamedType'.
        // We use std::declval to also 'recreate' an object of type 'Param'.
        // We can use both of these recreated objects to test the validity!
        template <typename... Params> constexpr auto test_validity(int /* unused */)
        -> decltype(std::declval<UnnamedType>()(std::declval<Params>()...), std::true_type())
        {
            // If substitution didn't fail, we can return a true_type.
            return std::true_type();
        }
    
        template <typename... Params> constexpr std::false_type test_validity(...)
        {
            // Our sink-hole returns a false_type.
            return std::false_type();
        }
    
    public:
        // A public operator() that accept the argument we wish to test onto the UnnamedType.
        // Notice that the return type is automatic!
        template <typename... Params> constexpr auto operator()(Params&& ...)
        {
            // The argument is forwarded to one of the two overloads.
            // The SFINAE on the 'true_type' will come into play to dispatch.
            return test_validity<Params...>(int());
        }
    };
    
    template <typename UnnamedType> constexpr auto is_valid(UnnamedType&& t) 
    {
        // We used auto for the return type: it will be deduced here.
        return container<UnnamedType>();
    }
    

This code is working even if my types are incomplete, for instance a forward declaration, or a normal declaration but with a missing definition. What can I do? Well, you can insert a check on the size of your type either in the SFINAE construction or before calling it: “static_assert( sizeof( T ), “type is incomplete.” );”.

Finally, why are using the notation “&&” for the lambdas parameters? Well, these are called forwarding references. It’s a really complex topic, and if you are interested, here is good article about it. You need to use “auto&&” due to the way declval is working in our is_valid implementation!

  • 即使我的类型不完整,例如前向声明或缺少定义的普通声明,此代码也可以工作。我能做什么?嗯,你可以在 SFINAE 构造中或在调用它之前插入一个对类型大小的检查:“static_assert(sizeof(T),”type is incomplete。);”。

  • 最后,为什么使用符号“&&”作为lambdas参数?这些被称为转发引用。这是一个非常复杂的话题。

Notes:

This is my first serious article about C++ on the web and I hope you enjoyed it! I would be glad if you have any suggestions or questions and that you wish to share with me in the commentaries.

Anyway, thanks to Naav and Superboum for rereading this article and theirs suggestions. Few suggestions were also provided by the reddit community or in the commentaries of this post, thanks a lot guys!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值