C++

C++ 模板

Traits, SFINAE, CRTP

Posted by w@hidva.com on April 14, 2016

type_traits

SFINAE

  • SFINAE;Substitution failure is not an error,替代失败不是一个错误;即编译器在实例化模 板时若无法推导出模板实参的值,或者无法确定函数的原型;则认为替代失败,此时并不会视为一个编译错误, 而是将无法推导的模板函数从候选函数集中移除,继续尝试下一个模板函数.所以:

    template <bool B,typename T = void>
    struct enable_if {};
    
    template <typename T>
    struct enable_if<true,T> {
        using type = T;
    };
    
    template<typename T,typename enable_if<std::is_integral<T>::value, int>::type n = 0>
    void what_am_i(T)  // 1
    {
        puts("integral");
        return ;
    }
    
    template<typename T,typename enable_if<std::is_floating_point<T>::value, int>::type n = 0>
    void what_am_i(T) // 2
    {
        puts("floating point");
        return ;
    }
    
    template<typename T,typename enable_if<std::is_pointer<T>::value, int>::type n = 0>
    void what_am_i(T) // 3
    {
        puts("pointer");
        return ;
    }
    
    /* 此时字面值 123 的类型是 int 类型,所以编译器在使用 int 类型来实例化 2,3 时,均会在确定模
     * 板实参类型过程中替代失败,所以 2,3 都将被丢弃;所以选择了 1.事实上对于任何 integral 类型,
     * 将都只能实例化 1,而无法实例化 2,3.
     */
    what_am_i(123);
    
    template <typename T>
    typename enable_if<is_integral<T>::value,void>::type
    what_am_i(T) // 1
    {
        puts("integral");
        return ;
    }
    
    template <typename T>
    typename enable_if<is_pointer<T>::value,void>::type
    what_am_i(T) // 2
    {
        puts("pointer");
        return ;
    }
    
    /* 此时在实例化模板函数 2 的过程中,虽然可以确定模板实参 T 的取值:int;但是在确定模板函数的原
     * 型时替代出错,所以此时将跳过 2.
     */
    what_am_i(123);
    
  • SFINAE 仅在确定模板实参,以及函数原型时才能用;若替代失败出现在函数体内,则会认为是编译错误!如:

    template <typename T>
    void what_am_i(T) // 1
    {
        typename enable_if<is_integral<T>::value,int>::type i = 0;
        puts("integral");
        return ;
    }
    
    /* 此时字面值"123."是 double 类型,会在实例化 1 中在函数体内出现替代失败,此时编译器会提示
     * 出错.
     */
    what_am_i(123.);
    

模板重载

  • 关于这块,不是很全面,仅是从几个现象上来理解模板重载是个啥子情况;我可以想象出标准上针对模板重载一 定定义了一绷子规则.

  • 重定义;对于同名模板函数,若模板形参表,函数形参表,函数返回值类型相同,则认为重定义了.如下:

    template <typename T>
    void f(T a)
    {
    }
    
    template <typename T>
    void f(T a)
    {
        return 0;
    }
    
    // 错误!重定义.
    

    若三者有一处不同,则不会被认为重定义,如下:

    template <typename T>
    void f(T a)
    {
    }
    
    template <typename T>
    int f(T a)
    {
        return 0;
    }
    
    // 编译通过,不会提示重定义;但是若 f() 被实例化,则肯定会提示二义性.
    

CRTP

  • CRTP,curiously recurring template pattern,奇异递归模板模式;即将派生类作为基类的模板参 数.如:

    template <typename DerivedT>
    struct Base {
        // ...
    };
    
    struct Derived1 : public Base<Derived1> {
        // ...
    };
    
  • CRTP,利用了模板的以下特性:

    • 仅在模板被用到时,才会实例化;因此基类中的模板函数仅会在派生类对象调用她们的时候才会实例化, 而此时派生类的定义已经完全可见了!

    注意在基类的数据成员中不能有派生类类型,因为这样会导致编译器无法计算派生类对象所占字节大小.实际上 只要编译器可以计算出派生类对象的内存布局,那么基类中可以随意.

使用场景

  • 实现静态多态;

    首先看一下使用动态多态下的实现:

    struct Base {
    
        /* 统一接口,派生类只需要继承 Base 然后实现自己的 implementionX() 函数即可.
         */
        void interface()
        {
            if (implemention0()) {
                // ...
            } else if (implemention1()) {
                // ...
            } else if (implemention2()) {
                // ...
            }
            return ;
        }
    
    protected:
        virtual bool implemention0() = 0;
        virtual bool implemention1() = 0;
        virtual bool implemention2() = 0;
    };
    
    struct Derived1 :  public Base {
        bool implemention0() override
        {
            return true;
        }
    
        bool implemention1() override
        {
            return true;
        }
    
        bool implemention2() override
        {
            return true;
        }
    };
    
    Derived1 d;
    // 此时 interface() 内部的 implementionX() 将使用 Derived1 重新实现的版本
    d.interface();
    

    再看一下,使用 CRTP 的静态多态实现,与动态多态相比,静态多态没有虚函数表,动态绑定这些负担.

    template <typename DerivedT>
    struct Base {
    
        void interface()
        {
            DerivedT *derived_this = static_cast<DerivedT*>(this);
            if (derived_this->implemention0()) {
                // ...
            } else if (derived_this->implemention1()) {
                // ...
            }
            return ;
        }
    
    };
    
    struct Derived1 : public Base<Derived1> {
    
        bool implemention0()
        {
            return true;
        }
    
        bool implemention1()
        {
            return true;
        }
    
    };
    
    Derived1 d;
    // 编译器在此时才会实例化,而且此时 Derived1 对象的内存布局都已经确定了.
    d.interface();
    
  • 多态复制构造;当使用多态时,常需要基于基类指针创佳对象的一份拷贝.常见办法是基类中增加clone() 虚函数,然后在每一个派生类中重写实现.使用CRTP,可以避免在派生类中增加这样的虚函数.

    普通实现:

    struct Base {
        virtual Base* clone() = 0;
    };
    
    struct Derived1 : public Base {
        Derived1* clone() override
        {
            return new Derived1(*this);
        }
    };
    
    struct Derived2 : public Base {
        Derived2* clone() override
        {
            return new Derived2(*this);
        }
    };
    

    使用 CRTP 的实现:

    struct Base {
        virtual Base* clone() = 0;
    };
    
    template <typename DerivedT>
    struct DerivedCloneHelper : public Base {
        Base* clone() final override
        {
            DerivedT *derived_this = static_cast<DerivedT*>(this);
            return new DerivedT(*derived_this);
        }
    };
    
    struct Derived1 : public DerivedCloneHelper<Derived1> {
    };
    
    struct Derived2 : public DerivedCloneHelper<Derived2> {
    };
    

    这里我本来以为模板类与虚函数同时使用可能不被允许呢;然而是可以的.

    因为这里并不影响Derived1,Derived2的内存布局;

    不过确实在Derived1,Derived2定义期间;clone()在其虚函数表中的填充地址无法确定,因为此时 尚未实例化clone(),但是这里并不影响Derived1,Derived2的内存布局;所以编译器可以在Derived1, Derived2完全可见时,再来实例化clone(),然后填充其在虚函数表中项.

参考

转载请注明出处!谢谢