编译时编程(Compile-Time Programming)

C++ 有很多方式可以实现编译时计算,而模板为编译时计算提供了更多的可能。C++ 有多个特性来支持编译时编程:

  • 在 C++98 之前,模板已经提供了编译时计算的能力,包括使用循环和执行路径选择。(然而,有人认为这是在滥用模板特性,因为它的语法不直观。)
  • 通过模板偏特化,我们可以在编译时根据特定的限制或需求选择不同的类模板实现。
  • 通过 SFINAE 原则,我们可以为不同的类型或限制选择不同的函数模板实现。
  • 在 C++11 和 C++14 中,可以在 constexpr 特性中使用直观的执行路径选择和大多数的语句类型(C++14,包括 for 循环、switch 等)让编译时计算支持的越来越好。
  • C++17 引入了 “编译时 if ”,可以根据编译时的条件或限制来弃用一些语句。这甚至可以在模板之外使用。

本文将介绍这些特性。

模板元编程

不同于动态语言在运行时泛化,模板是在编译时实例化。事实证明,C++ 模板的一些特性可以与实例化过程结合起来,在 C++ 语言本身中产生一种原始的递归 “编程语言” 。因此,模板可以用于计算程序的结果。例如,下面的代码用于计算给定的数是否为质数:

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; };

例如对于下面的表达式

IsPrime<9>::value

被扩展为:

9%4!=0 && 9%3!=0 && 9%2!=0

结果为 false

这里给出模板元编程的简单应用,关于模板元编程在后续章节还有详细介绍。

利用 constexpr 计算

C++11 引入的 constexpr 大大简化了多种形式的运行时计算。然而 C++11 的 constexpr 函数有严格的限制(例如 constexpr 函数被限制只能有一条 return 语句组成。),大多数的这些限制在 C++14 中被取消了。当然,能够在编译时成功计算的前提条件是所有的计算步骤在编译时可能的,否则只能在运行时才计算出结果。

例如,使用 C++11 的 constexpr 计算质数:

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++11 的 constexpr 函数被限制只能有一条 return 语句,我们这里利用了问号表达式的方法实现。C++14 去除这个限制,实现起来会更加容易,下面是使用 C++14 的 constexpr 函数来实现:

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
}

例如 isPrime(9),在编译时即可计算出结果。而下面的例子只能在运行时计算:

int x;
...
std::cout << isPrime(x); // evaluated at run time

偏特化的执行路径选择

编译时编程的一个应用是使用模板偏特化来选择不同的模板实现。例如,我们可以根据模板参数是否为质数来选择不同的实现:

// primary helper template:
template<int SZ, bool = isPrime(SZ)>
struct Helper;

// implementation if SZ is not a prime number:
template<int SZ>
struct Helper<SZ, false>
{
  ...
};

// implementation if SZ is a prime number:
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; // implementation depends on whether array has prime number as size
  ...
}

这里,根据 std::array<> 的 size 是否为质数,选择不同的 Helper 实现。

由于函数模板不支持偏特化,可以使用以下机制根据某些限制改变模板的实现:

  • 使用不同静态函数的类模板,
  • 使用 std::enable_if
  • 使用 SFINAE 特性(下面会介绍),
  • 使用编译时 if 特性,这是 C++17 引入的新特性(下面会介绍)。

SFINAE(Substitution Failure Is Not An Error)

在 C++ 中,根据不同的参数类型重载函数十分常见。编译器遇到一个重载函数的调用时,会根据实参的类型匹配最佳的实现。编译器在编译时决定函数调用时,如果重载中包含函数模板,就会进行模板特化,也即将模板函数的参数和返回值类型根据实参进行替换,如果替换失败,也即模板特化失败,直接忽略这次替换,不会导致编译错误,这就是 ”替换失败并非错误“ ,也即 SFINAE(Substitution Failure Is Not An Error)。

这里 给了一个很好的例子,利用 SFINAE 在编译时判断一个类型是否有iterator:

template <typename T>
struct has_iterator {
    template <typename U>
    static char test(typename U::iterator* x);
    template <typename U>
    static long test(U* x);
    static const bool value = sizeof(test<T>(0)) == 1;
};

int main() {
   has_iterator<vector<int> > test;
   if( test.value )
       cout << "vector have iterator" << endl;
   else
       cout << "vector not have iterator";
   has_iterator<int> test2;
   if( test2.value )
       cout << "int have iterator" << endl;
   else
       cout << "int not have iterator" << endl;
}

另外像 C++ 中的 std::enable_ifstd::is_classstd::void_t 以及 C++11 的 type traits 都是使用了 SFINAE 特性,例如 std::is_class 的实现:

template<typename T>
class is_class {
    typedef char yes[1];
    typedef char no [2];
    template<typename C> static yes& test(int C::*); // selected if C is a class type
    template<typename C> static no&  test(...);      // selected otherwise
  public:
    static bool const value = sizeof(test<T>(0)) == sizeof(yes);
};

再看下面这个例子:

// 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],代表一个有 NT 类型的数组。
  • 第二个函数模板的参数类型申明为 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

当传递一个 std::vector<> 时,匹配第二个函数模板:

std::vector<int> v;
std::cout << len(v); // OK: only len() for a type with size_type matches

当传递一个原始数组时,两个模板都不匹配:第一个模板入参是数组,显然不匹配;第二个模板返回值要求 T 类型有 size_type 成员,显然也不匹配。由于 SFINAE 特性,替换过程不会产生报错,但是编译器会产生 len() 没有找到的报错。

int* p;
std::cout << len(p); // ERROR: no matching len() function found

当传一个 std::allocator<int> 时,std::allocator<int>size_type 成员,第二个函数模板匹配成功,但是 std::allocator<int> 没有 size() 成员,这个时候第二个函数模板不会被忽略,编译器会产生没有 size() 函数的报错。

std::allocator<int> x;
std::cout << len(x);  // ERROR: len() function found, but can’t size()

如果增加一个更加通用的 len() 函数:

// 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;
}

这里新增的第三个通用 len() 函数,总是匹配的,但是最差的匹配。例如:

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

对于原始指针,只有第三个 len() 是匹配的。但是对于 std::allocator,第二个和第三个函数都匹配,但是第二个更加匹配,因此,编译器还会报没有 size() 的错误。

对于 传递 std::allocator 时,如果我们不想让第二个函数模板匹配,也就是说对于有 size_type 成员而没有 size() 成员的类型,在替换过程中忽略第二个成员函数,一种有效处理如下:

  • 使用拖尾返回值类型语法,
  • 使用 decltype 和逗号操作符定义返回值类型,
  • 保证逗号操作符前的表达式有效,
  • 在逗号操作符的的最后定义真正的返回值类型。

例如,将第二个函数模板修改如下:

template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
  return t.size();
}

传递 std::allocator 将不会匹配第二个函数模板,这时只有第三个通用的 len() 匹配:

std::allocator<int> x;
std::cout << len(x); 

编译时 if

借助偏特化、SFINAE 和 std::enable_if,我们可以使能或者使无效模板实现。C++17 引入的编译时 if 语句可以根据编译时条件使能或者使无效特定的语句。例如,在可变函数模板中介绍的 print() 的例子,为了减少一个空参数的 print() (为了结束递归):

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() 的代码不会被实例化,递归结束。注意: if constexpr 也可以用于非模板的普通函数中。

至此,本文结束,更多 C++ 模板相关介绍,敬请期待!

参考:

  • http://www.tmplbook.com
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值