SFINAE

替换失败非错(substitution failure is not an error)
比如下面这段,会编译失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Bar {
    typedef double internalType;  
};

template <typename T> 
typename T::internalType foo(const T& t) { 
    cout << "foo<T>\n"; 
    return 0; 
}

int main() {
    foo(Bar());
    foo(0); // << error!
}

编译器执行过程

  • 名称查找
  • 模版参数推断,这里找不到不会抛错
  • 找到合适的函数
  • 找到的候选集合 为空,或者 > 1,则失败,否则执行这个匹配

可以把上面的例子改为 C++ 11 实现

1
2
3
4
5
6
7
// C++11:
template <class T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type 
foo(T t) {
  std::cout << "foo<arithmetic T>\n";
  return t;
}

这里会解析传入的函数参数类型,如果是 非算术类型,则拒绝
否则生成对应的类型T

C++ 14/17 中,对 std::is_arithmetic::value 做了简化

1
2
3
4
5
6
7
// C++17:
template <class T>
typename std::enable_if_t<std::is_arithmetic_v<T>, T> // << shorter!
foo(T t) {
  std::cout << "foo<arithmetic T>\n";
  return t;
}

完整的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <type_traits>

template <class T>
typename std::enable_if_t<std::is_arithmetic_v<T>, T> // << shorter!
foo(T t) {
  std::cout << "foo<arithmetic T>\n";
  return t;
}

template <class T>
typename std::enable_if_t<!std::is_arithmetic_v<T>, void>
foo(T t) {
  std::cout << "foo fallback\n";
}

int main() {
    foo(0);
    foo(std::string{});
}

标签分发

一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename T>
int get_int_value_impl(T t, std::true_type) {
    return static_cast<int>(t+0.5f);
}

template <typename T>
int get_int_value_impl(T t, std::false_type) {
    return static_cast<int>(t);
}

template <typename T>
int get_int_value(T t) {
    return get_int_value_impl(t, std::is_floating_point<T>{});
}

根据std::is_floating_point<T>判断是否满足条件

  • 当返回 std::true_type,调用 static_cast(t+0.5f)
  • 当返回 std::false_type,调用 static_cast(t)

在 C++ 17 中,可以使用if constexpr,实现同样的编译期计算功能

1
2
3
4
5
6
7
8
9
template <typename T>
int get_int_value(T t) {
     if constexpr (std::is_floating_point<T>) {
         return static_cast<int>(t+0.5f);
     }
     else {
         return static_cast<int>(t);
     }
}

C++ 20 中,用concept进一步简化代码,增加可读性

1
2
3
4
5
6
7
// define a concept:
template <class T>
concept SignedIntegral = std::is_integral_v<T> && std::is_signed_v<T>;

// use:
template <SignedIntegral T>
void signedIntsOnly(T val) { }

上面创建了 一个 concept为 SignedIntegral,它要满足条件:std::is_integral_v && std::is_signed_v
之后定义一个模版:template

concept

concept是 C++20 的新功能,在编译期在模板参数上的一些约束,可以用于类模版和函数模版,控制期函数重载和部分特化
这里新增了两个关键字

  • requires
  • concept

一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <concepts>

template <typename T>
concept Sortable = requires(T t) {
    std::sort(t.begin(), t.end());  // Requires the type to be sortable
};

template <typename Container>
requires Sortable<Container>
auto findMax(const Container& container) {
    return *std::max_element(container.begin(), container.end());
}

int main() {
    std::vector<int> numbersVec = {5, 1, 9, 3, 7};
    std::list<double> numbersList = {2.5, 1.1, 4.7, 3.3};

    int maxVec = findMax(numbersVec);
    double maxList = findMax(numbersList);

    std::cout << "Max in vector: " << maxVec << std::endl;
    std::cout << "Max in list: " << maxList << std::endl;
}

解释

  • 定义了一个 concept:Sortable,确保类型 T支持 std::sort
  • 函数findMax,对于任何满足 Sortable 约束的参数都可以工作
  • findMax 增加了约束:requires Sortable

编译 && 执行

1
2
3
4
g++ -o concept -std=c++20 concept.cpp -lstdc++

Max in vector: 9
Max in list: 4.7

另一个例子,检查模版参数是否包含std::string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template<typename T>
concept has_string_data_member = requires(T v) { 
    { v.name_ } -> std::convertible_to<std::string>; 
};

struct Person {
    int age_ { 0 };
    std::string name_;
};

struct Box {
    double weight_ { 0.0 };
    double volume_ { 0.0 };
};

int main() {
    static_assert(has_string_data_member<Person>);
    static_assert(!has_string_data_member<Box>);
}

约束条件可以有多个:

1
2
3
4
5
6
template <typename T>
concept Clock = requires(T c) { 
    c.start();  
    c.stop();
    c.getTime();
  };

从语言进化角度看

一个例子,假设对于整数,相等就直接比较 ==
如果是浮点数,如果两个数的 相减的 绝对者,小于一个阈值,则也认为是相等
C++11/14 中可以这么写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T> 
constexpr enable_if_t<is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return absolute(a - b) < static_cast<T>(0.000001);
}
template <class T>
constexpr enable_if_t<!is_floating_point<T>::value, bool> 
close_enough(T a, T b) {
   return a == b;
}

C++17 通过if constexpr可以让代码可读性更好

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <class T> constexpr T absolute(T arg) {
   return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T> constexpr bool close_enough(T a, T b) {
   if constexpr (is_floating_point_v<T>) // << !!
      return absolute(a - b) < precision_threshold<T>;
   else
      return a == b;
}

C++20 通过约束,让代码可读性进一步提升

1
2
3
4
5
template <typename T>
requires std::is_floating_point_v<T>
constexpr bool close_enough20(T a, T b) {
   return absolute(a - b) < precision_threshold<T>;
}

最上面 C++11/14的那个里子,也可以用C++98/03 实现,但是写起来会更复杂
而且现在也很少有人用这么老的编译器了,CentOS默认的编译器都是 4.8.5,可以支持 C++11了
C++98的对于模版的语法,就可以忽略了吧。。。

concept中最核心的就是约束的表达式

  • Conjunctions(与)
  • Disjunctions(或)
  • Atomic Constraints(原子约束)
1
2
3
4
5
template<class T> constexpr bool is_a = true;
template<class T> constexpr bool is_b = true;

template<class T>
concept concept_a_or_b = is_a<T> || is_b<T>;

concept_a_or_b含有2个原子约束,然后通过disjunctions组合而成

总结

总之,现代C++的目的之一,就是让代码的可读性越来越好
没有concept也可以用其他方式实现类似的功能,但是可读性就不行了

Concepts 通过将模板的类型约束抽象出来,然后在模板定义时再使用
这样成功解耦了模板类型约束和模板本身的一些类型逻辑

参考