美文网首页C++ Templates
【C++ Templates(8)】编译期编程

【C++ Templates(8)】编译期编程

作者: downdemo | 来源:发表于2018-04-22 17:42 被阅读68次
    • C++有许多支持编译期编程的特性
      • C++98前,模板提供了编译期计算的能力,包括使用循环和执行路径选择
      • 使用局部特化,可以在编译期根据特定的约束或要求,在不同的类模板实现之间进行选择
      • 使用SFINAE,允许在不同的函数模板实现之间对不同的类型或不同的约束进行选择
      • 在C++11和C++14中引入了constexpr特性,编译期计算得到了更好的支持
      • C++17引入了编译期if来禁用依赖于编译期条件或限制的语句,它在模板外也是适用的

    模板元编程

    • 模板在编译期实例化(与运行期实例化的动态语言相反),这就启发把模板的一些特性结合到实例化过程中,以此产生一系列递归。下例在编译期找出质数
    template<unsigned p, unsigned d>   // p: number to check, d: current divisor
    struct DoIsPrime {
        static constexpr bool value = (p%d != 0) && DoIsPrime<p,d-1>::value; 
    }; 
    
    template<unsigned p>               // end recursion if divisor is 2
    struct DoIsPrime<p,2> {
        static constexpr bool value = (p%2 != 0);
    }; 
    
    template<unsigned p>               // primary template
    struct IsPrime {
        // start recursion with divisor from  p/2:
        static constexpr bool value = DoIsPrime<p,p/2>::value;
    }; 
    
    // special cases (to avoid endless recursion with template instantiation):
    template<>
    struct IsPrime<0> { static constexpr bool value = false; }; 
    template<>
    struct IsPrime<1> { static constexpr bool value = false; }; 
    template<>
    struct IsPrime<2> { static constexpr bool value = true; }; 
    template<>
    struct IsPrime<3> { static constexpr bool value = true; }; 
    
    • 如果传递的模板参数p是质数,IsPrimer<>模板返回在成员value中,为此会实例化DoIsPrimer<>,它将递归扩展为一个用于检查每个在p/2和2之间的因数d的是否能整除p的表达式
    IsPrime<9>::value
    // 扩展为
    DoIsPrime<9,4>::value
    // 扩展为
    9%4!=0 && DoIsPrime<9,3>::value
    // 扩展为
    9%4!=0 && 9%3!=0 && DoIsPrime<9,2>::value
    // 扩展为
    9%4!=0 && 9%3!=0 && 9%2!=0
    // 因为9%3为0,结果为false
    

    使用constexpr计算

    • C++11的constexpr函数有许多严格的限制,如仅能由一条return语句组成,C++14移除了大多数限制。下面是C++11中检查质数的例子
    constexpr bool
    doIsPrime (unsigned p, unsigned d)  // p: number to check, d: current divisor
    {
        return d!=2 ? (p%d!=0) && doIsPrime(p,d-1) // check this and smaller divisors
                  : (p%2!=0);                    // end recursion if divisor is 2
    }
    
    constexpr bool isPrime (unsigned p)
    {
        return p < 4 ? !(p<2)             // handle special cases
                   : doIsPrime(p,p/2);  // start recursion with divisor from p/2
    }
    
    • C++14中则不限制只能写一条return语句
    constexpr bool isPrime (unsigned int p)
    {
        for (unsigned int d=2; d<=p/2; ++d) {
            if (p % d == 0) {
                return false;  // found divisor without remainder
            }
        }
        return p > 1;      // no divisor without remainder found
    }
    
    • 使用constexpr isPrimer()检查9是否为质数,调用如下
    isPrime(9)
    
    • 这能在编译期实现,但不是必须如此。在要求编译期值(如数组长度或非类型模板实参)的上下文中,将在编译期调用constexpr函数,如果不行则发出错误,在其他上下文中,不一定会在编译期计算,但如果计算失败则不会发出错误,并且调用被替换为运行期调用
    constexpr bool b1 = isPrime(9); // 编译期计算
    const bool b2 = isPrime(9); // 同样在编译期计算
    // 在块作用域中编译器会决定在编译期还是运行期计算
    bool fiftySevenIsPrime() {
        return isPrime(57); // 在编译期或运行期计算
    }
    // 下面无论x是否为质数都会在运行期计算
    int x;
    ...
    std::cout << isPrime(x); // 运行期计算
    

    使用局部特化的执行路径选择

    • 使用局部特化在编译期进行不同实现间的选择,比如根据模板实参是否为质数选择不同的实现
    // 基本辅助模板
    template<int SZ, bool = isPrime(SZ)>
    struct Helper;
    
    // 如果SZ不是质数的实现
    template<int SZ>
    struct Helper<SZ, false>
    {
        ...
    };
    
    // 如果SZ是质数的实现
    template<int SZ>
    struct Helper<SZ, true>
    {
        ...
    };
    
    template<typename T, std::size_t SZ>
    long foo (std::array<T,SZ> const& coll)
    {
        Helper<SZ> h; // 实现依赖于数组大小是否为质数
        ...
    }
    
    • 也可以使用基本模板实现一种情况,局部特化实现另一种情况
    // 基本辅助模板(如果没有特化匹配)
    template<int SZ, bool = isPrime(SZ)>
    struct Helper
    {
        ...
    };
    
    // 如果SZ是质数的实现
    template<int SZ>
    struct Helper<SZ, true>
    {
        ...
    };
    
    • 函数模板不支持局部特化,必须使用其他机制改变函数实现,可选如下
      • 使用带static函数的类
      • 使用std::enable_if<>
      • 使用SFINAE特性
      • 使用编译期if(C++17)

    SFINAE(Substitution Failure Is Not An Error)

    • 在重载解析的匹配中,为了防止替换过程产生无意义的错误,这样的替换会被忽略,这个原则就是SFINAE
    // number of elements in a raw array:
    template<typename T, unsigned N>
    std::size_t len (T(&)[N])
    {
        return N;
    }
    
    // number of elements for a type having size_type:
    template<typename T>
    typename T::size_type len (T const& t)
    {
        return t.size();
    }
    
    • 第一个函数模板声明参数为T(&)[N],代表参数必须是一个N个T类型元素的数组。第二个简单把参数声明为T,但返回类型为T::size_type,要求传递的实参必须有对应的size_type成员。当传递原始数组或字符串字面值时,只有第一个模板匹配
    int a[10];
    std::cout << len(a); // OK: only len() for array matches
    std::cout << len("tmp"); // OK: only len() for array matches
    
    • 根据签名,把T替换为int[10]和char const[4]也可行,但是返回类型T::size_type导致了隐藏的错误,因此这个模板会被忽略。当传递std::vector<>时,只有第二个模板匹配
    std::vector<int> v;
    std::cout << len(v); // OK: only len() for a type with size_type matches
    
    • 传递原始指针时,编译器将提示两个模板都不匹配
    int* p;
    std::cout << len(p); // ERROR: no matching len() function
    found
    
    • 这和传递一个有size_type成员但没有size()成员函数的对象不同,比如传递std::allocator<>类型对象时,编译器将匹配第二个函数模板,不会产生匹配错误,而是产生调用size()无效的错误,这次第二个模板不会被忽略
    std::allocator<int> x;
    std::cout << len(x); // ERROR: len() function found, but can't
    size()
    
    • 当替换返回类型无意义时,忽略一个匹配会造成编译器选择另一个参数更差的匹配
    // number of elements in a raw array:
    template<typename T, unsigned N>
    std::size_t len (T(&)[N])
    {
        return N;
    }
    
    // number of elements for a type having size_type:
    template<typename T>
    typename T::size_type len (T const& t)
    {
        return t.size();
    }
    
    // fallback for all other types:
    std::size_t len (...) // 这个省略号是参数包
    {
        return 0;
    }
    
    int a[10];
    std::cout << len(a); // OK: len() for array is best match
    std::cout << len("tmp"); //OK: len() for array is best match
    std::vector<int> v;
    std::cout << len(v); // OK: len() for a type with size_type is best match
    int* p;
    std::cout << len(p); // OK: only fallback len() matches
    std::allocator<int> x;
    std::cout << len(x); // ERROR: 2nd len() function matches
    best, but can't call size() for x
    
    • 对于allocator,第二个和第三个函数模板匹配,第二个是更好的匹配,但返回类型没有size()成员会产生错误
    • 如果把SFINAE机制用于确保函数模板在某种限制下被忽略,我们就说SFINAE out这个函数,当在C++标准中读到一个函数模板“不应该加入重载解析除非...”,就表示SFINAE被用来SFINAE out这个函数模板,比如类std::thread声明一个构造函数如下
    namespace std {
        class thread {
        public:
            ...
            template<typename F, typename... Args>
            explicit thread(F&& f, Args&&... args);
            ...
        };
    }
    
    • 并带有如下remark
    Remarks: This constructor shall not participate in overload resolution if decay_t<F> is the same type as std::thread.
    
    • 这表示这个模板构造函数被调用时,std::thread作为第一个和唯一一个实参,则这个模板会被忽略。调用一个线程时,通过SFINAE out这个模板来保证预定义的拷贝或移动构造函数总被使用。标准库提供了工具能轻松地禁用模板,最出名的特性是std::enable_if<>(它是用局部特化和SFINAE实现的)
    namespace std {
        class thread {
        public:
            ...
            template<typename F, typename... Args,
                typename =
            std::enable_if_t<!std::is_same_v<std::decay_t<F>,
    thread>>>
            explicit thread(F&& f, Args&&... args);
            ...
        };
    }
    
    • 指定正确的表达式来SFINAE out函数模板不会总是那么容易,比如想让函数模板len()对有size_type而没有size()的实参被忽略,之前的方案会产生错误
    template<typename T>
    typename T::size_type len (T const& t)
    {
        return t.size();
    }
    
    std::allocator<int> x;
    std::cout << len(x) << '\n'; // ERROR: len() selected, but
    x has no size()
    
    • 对此有一个惯用的解决方法
      • 使用尾置返回类型
      • 使用decltype和逗号运算符定义返回类型
      • 在逗号运算符开始处制定必须有效的全部表达式(转换为void以防逗号表达式被重载)
      • 在逗号运算符结尾处定义一个真正返回类型的对象
    template<typename T>
    auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
    {
        return t.size();
    }
    

    编译期if

    • C++17引入了编译期if语句,允许基于编译期条件,使用或禁用指定的语句
    template<typename T, typename... Types>
    void print (T const& firstArg, Types const&... args)
    {
        std::cout << firstArg << '\n';
        if constexpr(sizeof...(args) > 0) {
            print(args...); // code only available if sizeof...(args)>0 (since C++17)
        }
    }
    
    • 这里如果print()只有一个实参,args为空参数包,sizeof...(args)为0,递归调用print()的语句变为废弃语句,不会实例化,因此不会要求对应的函数存在,递归结束。不会实例化意味着第一编译解析阶段执行,检查语法正确性和不依赖于模板参数的名称,比如
    template<typename T>
    void foo(T t)
    {
        if constexpr(std::is_integral_v<T>) {
            if (t > 0) {
                foo(t-1); // OK
            }
        }
        else {
            undeclared(t); // error if not declared and not discarded (i.e. T is not integral)
            undeclared(); // error if not declared (even if discarded)
            static_assert(false, "no integral"); // always asserts (even if discarded)
            static_assert(!std::is_integral_v<T>, "no integral"); //OK
        }
    }
    
    • if constexpr能用于任何函数,不仅局限于模板,只需要一个产生布尔值的编译器表达式
    int main()
    {
        if constexpr(std::numeric_limits<char>::is_signed) {
            foo(42); // OK
        }
        else {
            undeclared(42); // error if undeclared() not declared
            static_assert(false, "unsigned"); // always asserts(even if discarded)
            static_assert(!std::numeric_limits<char>::is_signed, "char is unsigned"); //OK
        }
    }
    
    • 把这个特性用于编译期函数isPrimer(),如果给定大小不是质数则执行附加代码
    template<typename T, std::size_t SZ>
    void foo (std::array<T,SZ> const& coll)
    {
        if constexpr(!isPrime(SZ)) {
            ... //special additional handling if the passed array
            // has no prime number as size
        }
        ...
    }
    

    相关文章

      网友评论

        本文标题:【C++ Templates(8)】编译期编程

        本文链接:https://www.haomeiwen.com/subject/wnuclftx.html