前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >未来已来:从SFINAE到concepts

未来已来:从SFINAE到concepts

作者头像
高性能架构探索
发布2024-01-26 19:31:35
1510
发布2024-01-26 19:31:35
举报
文章被收录于专栏:技术随笔心得技术随笔心得

你好,我是乐哥,一个从事C/CPP开发十几年的老鸟~~

在开始正文之前,我们先看一个例子。

代码语言:javascript
复制
#include <string>
void fun(const auto& x) {
   std::string v = x;
}
int main() {
  fun(1);
  return 0;
}

emm,相信你也看出问题所在了,当然了,编译器也会提示如下错误:

代码语言:javascript
复制
error: no viable conversion from 'const int' to 'std::string' (aka 'basic_string<char>')
        std::string v = x;

也就是说,在fun()函数内部,将参数x赋值给一个string类型的v,但是在main()函数中 ,调用fun()函数时候传入了1,这个编译器会推导为int类型,那么把一个int类型赋值给string,编译器会报错。

方案

如果没接触过C++20,那么解决这种报错往往有两种方式:SFNIAEif constexpr

SFINAE

SFINAE 是 "Substitution Failure Is Not An Error" 的缩写。这是一种 C++ 中的编译期技术,用于在模板实例化过程中,当尝试进行模板参数的替换时,如果出现了替换失败(通常是由于找不到相应的成员函数、操作符等),不会导致编译错误,而是会选择其他可行的模板特化。

它的核心思想是,如果在模板参数的替换中遇到了错误,编译器不应该报错,而是应该简单地将这个特化从候选列表中移除。这样,即使部分模板特化失败,编译仍然可以继续进行,选择其他可行的特化。

这一机制使得在模板元编程中能够更加灵活地根据类型的特性选择不同的实现路径。std::enable_if 就是利用了 SFNIAE 的概念,通过在模板参数替换失败时移除特化,实现了在编译期间的条件选择。

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

// is_string 类型特性
template <typename T, typename Enable = void>
struct is_string : std::false_type {};

template <typename T>
struct is_string<T, std::enable_if_t<std::is_same_v<T, std::string> ||
                                      std::is_same_v<T, const char*> ||
                                      std::is_same_v<T, char[]>>> : std::true_type {};

template <typename T, typename = std::enable_if_t<is_string<T>::value, T>>
void fun(T t) {
  std::string v = t;
}


template <typename T, std::enable_if_t<!is_string<T>::value, bool> = true>
void fun(T t) {
  std::cout << "foo fallback\n";
}

int main() {
  fun(1);
  fun("abc");
  fun(1.2);
  return 0;
}

constexpr

if constexpr 是 C++17 中引入的一个关键字,用于在编译时进行条件判断,从而实现更灵活的模板元编程。与传统的 if 语句不同,if constexpr 中的条件表达式在编译时求值,只有符合条件的分支才会被保留,而不符合条件的分支在生成的代码中会被舍弃。

这种特性使得在模板中可以编写更加直观和灵活的代码,而不必依赖于模板元编程中的繁琐技巧,同时可以避免生成不必要的代码。如果使用if constexpr来重写我们上面的代码,则如下:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T, typename Enable = void>
struct is_string : std::false_type {};

template <typename T>
struct is_string<T, std::enable_if_t<std::is_same_v<T, std::string> ||
                                      std::is_same_v<T, const char*> ||
                                      std::is_same_v<T, char[]>>> : std::true_type {};

template <typename T>
void fun(T t) {
  if constexpr(is_string<T>::value) {
    std::string v = t;
  } else {
   // others
  }
}

int main() {
  fun(1);
  fun("abc");
  fun(1.22);
  return 0;
}

为了解决这个问题,或者说把问题暴露在编译阶段,自C++20起引入了concepts

横空出世

C++20 引入了概念(Concepts)这一新特性,它是一种用于约束模板类型参数的机制。概念提供了一种更加清晰和简洁的方法,用于规定模板类型参数必须满足的条件,以替代传统的通过模板特化和SFINAE(Substitution Failure Is Not An Error)技术实现的模板约束方式。

在前面的例子中,我们无非是通过各种方式来约束参数,使得满足某个条件的参数调用一个模板函数,而不满足的则使用另外一个模板函数。这种方式在C++20用的更为广泛,称之为约束模板参数

约束模板参数

约束模板参数类型的写法与传统的目标函数很像,如下这个是传统的模板函数:

代码语言:javascript
复制
template<typename T>
void fun() {
}

此时,如果要限制模板参数为整形,则可以像如下这样写:

代码语言:javascript
复制
template <std::integral T>
void fun(T x) {
  // ...
}

这样当传入fun()的为非int类型时候,编译器会报如下错误:

代码语言:javascript
复制
<source>: In function 'int main()':
<source>:9:8: error: no matching function for call to 'fun(const char [4])'
    9 |     fun("abc");
      |     ~~~^~~~~~~
<source>:5:6: note: candidate: 'template<class T>  requires  integral<T> void fun(T)'
    5 | void fun(T x) {
      |      ^~~
<source>:5:6: note:   template argument deduction/substitution failed:
<source>:5:6: note: constraints not satisfied
In file included from /opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/compare:37,
                 from /opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/bits/char_traits.h:56,
                 from /opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/string:42,
                 from <source>:1:
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/concepts: In substitution of 'template<class T>  requires  integral<T> void fun(T) [with T = const char*]':
<source>:9:8:   required from here
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/concepts:100:13:   required for the satisfaction of 'integral<T>' [with T = const char*]
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/concepts:100:24: note: the expression 'is_integral_v<_Tp> [with _Tp = const char*]' evaluated to 'false'
  100 |     concept integral = is_integral_v<_Tp>;

相信很多人跟我一样,对于std::integral有点陌生,很容易写成std::integer,借用cppreference对其的定义:

代码语言:javascript
复制
template< class T >
concept integral = std::is_integral_v<T>;

template< class T >
inline constexpr bool is_integral_v = is_integral<T>::value;

也就是说intergral是一个concept,其约束条件成立的前提是T是一个int类型。

concepts

在C++20中,Concepts(概念)是一种对类型进行约束的机制。Concepts 允许程序员定义对类型进行断言的语法,这样在模板中可以使用这些断言来约束模板参数,使得只有满足特定条件的类型才能匹配模板。

concept形如:

代码语言:javascript
复制
template<typename T>
concept xxx = bool expression;

可以将其分为如下几个部分:

•模板参数列表•关键字concept•concept后跟名称•bool类型表达式

现在使用concepts重新编写我们前面的例子:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept is_string = std::is_same_v<T, std::string> ||
                   std::is_same_v<T, const char*> ||
                   std::is_same_v<T, char[]>;

template <typename T>
void fun(T t) {
  if constexpr (is_string<T>) {
    std::string v = t;
    std::cout << "String: " << v << std::endl;
  } else {
    std::cout << "Other type" << std::endl;
  }
}

int main() {
  fun(1); 
  fun("abc"); 
  fun(1.22);
  return 0;
}

concept除了与if constexpr结合使用,另外一种场景是与requires配合。

requires

借用cppreference对requires的描述,其有两种形式:

代码语言:javascript
复制
requires { requirement-seq } // 形式一
requires ( parameter-list (optional) ) { requirement-seq }    // 形式二

针对上面的例子,结合requires实现的话,有如下两种:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept StringType = 
    std::is_same_v<T, std::string> ||
    std::is_same_v<T, const char*> ||
    std::is_same_v<T, char[]>;

template <typename T>
requires StringType<T>
void fun(T t) {
    
}

int main() {
    //fun(1); 
    fun("abc");
    //fun(1.22); 
    return 0;
}

PS:requires可以在函数名前,也可以在之后,形如:

代码语言:javascript
复制
emplate <typename T>
requires CONDITION
void DoSomething(T param) { }

template <typename T>
void DoSomething(T param) requires CONDITION { }

上面这种写法使用了requires两种形式中的第一种,即在模板函数fun()中,要求其类型为string()(requires StringType)。

当然了,也可以像如下这样编写:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept StringType = 
    std::is_same_v<T, std::string> ||
    std::is_same_v<T, const char*> ||
    std::is_same_v<T, char[]>;

template <StringType T>
void fun(T t) {
    
}

int main() {
    //fun(1); 
    fun("abc");
    //fun(1.22); 
    return 0;
}

如果使用第二种形式的话,可以像如下这样写:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept StringType = requires (T t){
    requires std::is_same_v<T, std::string> ||
    std::is_same_v<T, const char*> ||
    std::is_same_v<T, char[]>;
};

template <StringType T>
void fun(T t) {
    
}

int main() {
    //fun(1); 
    fun("abc");
    //fun(1.22); 
    return 0;
}

也可以这样写:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept StringType = requires (T t){
    { t } ->std::convertible_to<std::string>;
};

template <StringType T>
void fun(T t) {
    
}

int main() {
    //fun(1); 
    fun("abc");
    //fun(1.22); 
    return 0;
}

在这个例子中,使用了std::convertible_to<std::string>

std::convertible_to<T, U> 是 C++20 中的一个概念(Concept),用于指定类型 T 是否可以隐式转换为类型 U

具体来说,std::convertible_to<std::string> 表示类型 T 是否可以隐式转换为 std::string 类型。如果满足这个概念,那么说明类型 T 可以在需要 std::string 类型的地方进行隐式转换。

如果对f(1)进行编译,错误提示如下:

代码语言:javascript
复制
<source>:17:5: error: no matching function for call to 'fun'
   17 |     fun(1); 
      |     ^~~
<source>:12:6: note: candidate template ignored: constraints not satisfied [with T = int]
   12 | void fun(T t) {
      |      ^
<source>:11:11: note: because 'int' does not satisfy 'StringType'
   11 | template <StringType T>
      |           ^
<source>:7:13: note: because type constraint 'std::convertible_to<int &, std::string>' was not satisfied:
    7 |     { t } ->std::convertible_to<std::string>;
      |             ^
/opt/compiler-explorer/clang-17.0.1/bin/../include/c++/v1/__concepts/convertible_to.h:27:26: note: because 'is_convertible_v<int &, std::string>' evaluated to false
   27 | concept convertible_to = is_convertible_v<_From, _To> && requires { static_cast<_To>(std::declval<_From>()); };
      |                          ^

错误提示在上面已经很清楚,这是因为fun(1)的时候,1不能转换成std::string导致。

cpperference对std::convertible_to的定义如下:

代码语言:javascript
复制
template< class From, class To >
concept convertible_to =
    std::is_convertible_v<From, To> &&
    requires {
        static_cast<To>(std::declval<From>());
    };

即可以从From转换成To。在这个定义中用到了concept和requires

熟能生巧

下面通过一些简单的例子来加深对concepts&requires的理解。

成员变量

判断一个对象是否存在某个成员变量:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept has_x = requires (T v) {
    v.x;
};
template <typename T>
concept has_x_y = requires (T v) {
    v.x;
    v.y;
};

void fun(has_x auto x) {}
void fun(has_x_y auto x) {}

struct X {
    int x;
};

struct Y {
    int x;
    int y;
};

int main() {
    fun(X{});
    return 0;
}

如果加上fun(Y{}),那么编译器就会报如下错误:

代码语言:javascript
复制
<source>: In function 'int main()':
<source>:29:8: error: call of overloaded 'fun(Y)' is ambiguous
   29 |     fun(Y{});
      |     ~~~^~~~~
<source>:15:6: note: candidate: 'void fun(auto:16) [with auto:16 = Y]'
   15 | void fun(has_x auto x) {}
      |      ^~~
<source>:16:6: note: candidate: 'void fun(auto:17) [with auto:17 = Y]'
   16 | void fun(has_x_y auto x) {}
      |      ^~~

从上面错误提示可以看出,在调用函数fun(Y{})的时候,参数为has_x 和has_x_y都匹配到了,即编译器不确定要使用哪个或者优先使用哪个,所以干脆报错完事~~

编译器有个特性,在候选集中往往选择那个最最匹配的,针对这个特性,我们修改上述代码如下:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
concept has_x = requires (T v) {
    v.x;
};
template <typename T>
concept has_x_y = has_x<T> && requires (T v) {
    v.y;
};

void fun(has_x auto x) {}
void fun(has_x_y auto x) {}

struct X {
    int x;
};

struct Y {
    int x;
    int y;
};

int main() {
    fun(X{});
    fun(Y{});
    return 0;
}

即在concepts has_x_y中,把has_x从之前的代码中拆分出来,作为一个条件子集,这样当编译器在编译的时候,发现有两个候选集,但是上面这个候选者更为合适(满足has_x和v.y),那么遂选择该候选者。

成员函数

如果要判断某个类是否存在某个成员函数,那么可以像如下这么写:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>


template <typename T>
concept FuncCall =
requires(T t){
  t.Func();
};

struct C {
  void Func() {}
};

template<typename T>
requires FuncCall<T>
void Func(T t) {
    t.Func();
}



int main() {
    C c;
    Func(c);
    return 0;
}

如果要判断成员函数返回类型,则可以:

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <type_traits>


template <typename T>
concept FuncCall =
requires(T t){
  {t.Func() } -> std::convertible_to<void>;
};

struct C {
  void Func() {}
};

template<typename T>
requires FuncCall<T>
void Func(T t) {
    t.Func();
}



int main() {
    C c;
    Func(c);
    return 0;
}

以上~~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方案
    • SFINAE
      • constexpr
      • 横空出世
        • 约束模板参数
          • concepts
            • requires
            • 熟能生巧
              • 成员变量
                • 成员函数
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档