世界变化太快,C++的各种写法,转眼都快看不懂了。通过傻瓜例子简单梳理些 C++ 基础内容
什么是泛型编程(Generic Programming)?
泛型编程是一种编程范式,其核心思想是编写与类型无关的代码,从而提高代码的复用性和灵活性。
几乎所有现代编程语言都提供了泛型编程的支持,某些语言(如 Java、C#)中称为“泛型(Generics)”的机制,通常通过类型擦除或运行时检查来实现;某些语言(如 Haskell)使用“参数多态(Parametric Polymorphism)”来描述泛型编程。但在 C++ 中,泛型编程实现被称为模板(Templates),是基于编译期实例化的。C++标准库的大量组件(如容器、算法、智能指针等)都是使用模板实现的。
C++ 模板主要有两种形式:
- 函数模板:定义与类型无关的通用函数。
- 类模板:定义与类型无关的通用类。
C++ 模板具有下列特色:
- 编译期展开:模板在编译期被实例化为具体类型的代码,从而提高性能。
- 模板特化:允许为某些类型提供特定实现,包括完全特化和部分特化。
- 模板元编程:允许在编译期进行复杂的计算和类型推导。
- 可变参数模板:允许模板接收任意数量的参数。
- 概念(Concepts):C++20 引入的特性,用于约束模板参数的类型和行为。
模板元编程和编译期展开,超出了泛型编程的范畴。它们用来在编译期优化代码或进行复杂计算,增加灵活性的同时,也造就了C++模板系统的高复杂性。
Concepts
concept 是一个编译期的布尔值表达式,用来描述类型的要求。你可以使用 template 语法来定义 concept。
| template <typename T>
concept MyConceptName = /* 布尔表达式 */;
|
其中,布尔表达式可以是任何可以在 编译期计算的表达式 ,通常是类型特征或其他概念的组合。
C++20 提供了许多标准库中的 concepts,比如:
std::integral
:表示一个类型是整型。
std::floating_point
:表示一个类型是浮点型。
std::convertible_to<T>
:表示一个类型可以隐式转换为 T。
std::same_as<T, U>
:表示两个类型是相同的。
requires 表达式在 concept 中使用,用于检查类型是否满足某些操作或行为:
| requires (参数列表) {
表达式1;
表达式2;
// 更多表达式...
}
|
表达式包含一系列需要检查的操作。如果所有操作都合法,则 requires 表达式返回 true,否则返回 false。
例子概览
文字太枯燥!!本文通过一些简短的例子展示了从 C++11 到 C++20 的泛型编程演进。我们重点介绍了函数模板、类模板、非类型模板参数以及 concepts 的使用场景。随着 C++20 的发布,泛型编程变得更加简洁和强大,尤其是 concepts 的引入大大提高了模板代码的可读性和维护性。
函数模板(Function Templates)
一个函数定义,C++整出这么多写法,难怪劝退这么多人
例子1 - 自动推导类型的加法函数
定义一个泛型的加法函数看看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | #include <iostream>
#include <string>
auto add(auto a, auto b) {
return a + b;
}
int main() {
using namespace std::string_literals;
std::cout << add(1, 1.1) << std::endl; //output: 2.1
std::cout << add("123"s, "456"s) << std::endl; //output: 123456
// std::cout << add("123"s, 456) << std::endl;
}
|
但是这个例子需要C++20支持(叫做Abbreviated Function Templates),
而对于C++14和C++17来说,需要写成下面这样(auto具备推导返回值类型能力):
| template<typename T, typename U>
auto add(T a, U b) {
return a + b;
}
|
而到了C++11,单独auto也不行了,它只是一个占位符,需要使用 decltype
来推导返回值类型:
| template<typename T, typename U>
auto add(T a, U b) -> decltype(a+b) {
return a + b;
}
|
C++98更为原始,auto含义都不一样,应该无需多提了。
例子2 - 只接受整数类型
定义一个只接受整数类型(比如 int, short, long, long long, char, ....)的add函数
1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <iostream>
#include <concepts>
auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
int main() {
std::cout << add(1, 255) << std::endl; //output 256
std::cout << add(1, 255ull) << std::endl; //output 256
std::cout << add(1, '\xff') << std::endl; //output 0 or 256
std::cout << add('\1', '\xff') << std::endl; // output 0 or 256
}
|
用concept取代 typename/class 来约束模板参数,add
函数可以写成这样:
| template<std::integral T, std::integral U>
auto add(T a, U b) {
return a + b;
}
|
也可以这样:
| template<typename T, typename U>
requires std::integral<T> && std::integral<U>
auto add(T a, U b) {
return a + b;
}
|
还可以这样:
| template<typename T, typename U>
auto add(T a, U b) requires std::integral<T> && std::integral<U> {
return a + b;
}
|
这些都用到了C++20引入的concept。上面的std::integral
是标准库提供的concept,requires
是其引入的关键词。
在C++17下面,可以使用 std::enable_if
:
| template<typename T, typename U, typename = typename std::enable_if_t<std::is_integral_v<T> && std::is_integral_v<U>>>
auto add(T a, U b) {
return a + b;
}
|
模板第一个默认参数是一个占位符参数 typename = typename std::enable_if_t<condition>
。使用的 SFINAE(Substitution Failure Is Not An Error)的技术,condition 是否为 true 决定了是否会实例化。
如果不使用上面的占位符参数,可以用返回值做文章:
| template<typename T, typename U>
typename std::enable_if_t<std::is_integral_v<T> && std::is_integral_v<U>, decltype(T{} + U{})>
add(T a, U b) {
return a + b;
}
|
或者static_assert
| template<typename T, typename U>
auto add(T a, U b) {
static_assert(std::is_integral_v<T> && std::is_integral_v<U>, "T and U must be integral");
return a + b;
}
|
在C++14和C++11下,没有std::enable_if_t
和 std::is_integral_v
,只能:
| template<typename T, typename U, typename = typename std::enable_if<std::is_integral<T>::value && std::is_integral<U>::value>::type>
auto add(T a, U b) {
return a + b;
}
|
或
| template<typename T, typename U>
typename std::enable_if<std::is_integral<T>::value && std::is_integral<U>::value, decltype(T{} + U{})>::type
add(T a, U b) {
return a + b;
}
|
同样,static_assert
也能用。
无论concept还是用SFINA,约束的好处:
- 提高模板代码可维护性(可读性?)
- 优化编译速度(可提前检测模板参数有效性)
尽管 SFINAE 可以在编译期有效地过滤不满足条件的模板实例,但编译器产生的错误信息通常较为复杂且难以理解。而 C++20 的 concepts 不仅可以提高代码的可读性,还能生成更友好的编译器错误信息。例如在使用不匹配的类型时,编译器会明确告诉你哪种类型不符合期望的概念。
例子3 - 确保参数类型一致
接前面的add函数,如何保证a、b两个参数类型完全一致??
Abbreviated Function Templates方式写出来:
| auto add(std::integral auto a, std::integral auto b)
requires std::same_as<decltype(a), decltype(b)> {
return a + b;
}
|
或
| template<std::integral T, std::integral U>
requires std::same_as<T, U>
auto add(T a, U b) {
return a + b;
}
|
或
| template<std::integral T, std::integral U>
auto add(T a, U b) requires std::same_as<T, U> {
return a + b;
}
|
尽管不如常规单模板参数写法简单:
| template<std::integral T>
T add(T a, T b) {
return a + b;
}
|
但比下面写法舒服:
| template<typename T, typename U>
auto add(T a, U b) requires std::integral<T> && std::integral<U> && std::same_as<T, U> {
return a + b;
}
|
Concept写法够乱了,SFINAE 的写法先不列了
例子4 - Concepts与重载
模板函数重载
使用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
28
29 | #include <iostream>
#include <string>
#include <concepts>
template<std::integral T>
auto add(T a, T b) {
std::cout << "Adding integers: ";
return a + b;
}
template<std::floating_point T>
auto add(T a, T b) {
std::cout << "Adding floats: ";
return a + b;
}
template<typename T>
T add(T a, T b)
{
std::cout << "Other: ";
return a + b;
}
int main()
{
std::cout << add(1, 1) << std::endl;
std::cout << add(1.0, 1.0) << std::endl;
std::cout << add(std::string("1"), std::string("1")) << std::endl;
}
|
不用Concept的话,add函数也能写,就是不够简洁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
add(T a, T b) {
std::cout << "Adding integers: ";
return a + b;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
add(T a, T b) {
std::cout << "Adding floats: ";
return a + b;
}
template<typename T>
typename std::enable_if<!std::is_integral<T>::value && !std::is_floating_point<T>::value, T>::type
add(T a, T b) {
std::cout << "Other: ";
return a + b;
}
|
例子5 - 标签派发(Tag Dispatching)与重载
在某些情况下,模板重载可能会导致代码过于复杂,特别是需要处理多种类型的情况下。标签派发(Tag Dispatching) 提供了一种更为直观、可维护的方式来处理不同类型的重载。
用C++20来写:
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
28
29
30
31 | struct MyIntegersTag {};
struct MyFloatsTag {};
auto add_impl(auto a, auto b, MyIntegersTag) {
std::cout << "Adding integers: ";
return a + b;
}
auto add_impl(auto a, auto b, MyFloatsTag) {
std::cout << "Adding floats: ";
return a + b;
}
auto add_impl(auto a, auto b)
{
std::cout << "Other: ";
return a + b;
}
auto add(auto a, auto b)
{
if constexpr (std::is_integral_v<decltype(a)> && std::is_integral_v<decltype(b)>) {
return add_impl(a, b, MyIntegersTag{});
}
else if constexpr (std::is_floating_point_v<decltype(a)> && std::is_floating_point_v<decltype(b)>) {
return add_impl(a, b, MyFloatsTag{});
}
else {
return add_impl(a, b);
}
}
|
其中定义两个空struct作为标签(Tag)。
如果不单独写个函数来让它派发,也可以手动来:
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
28
29
30 | #include <iostream>
#include <string>
#include <concepts>
struct MyIntegersTag {};
struct MyFloatsTag {};
auto add(auto a, auto b, MyIntegersTag) {
std::cout << "Adding integers: ";
return a + b;
}
auto add(auto a, auto b, MyFloatsTag) {
std::cout << "Adding floats: ";
return a + b;
}
auto add(auto a, auto b)
{
std::cout << "Other: ";
return a + b;
}
int main()
{
std::cout << add(1, 1, MyIntegersTag{}) << std::endl;
std::cout << add(1.0, 1.0, MyFloatsTag{}) << std::endl;
std::cout << add(std::string("1"), std::string("1")) << std::endl;
}
|
例子6 - if constexpr与特化
假定我们需要一个将类型T转化为std::string的函数,在C++20下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | #include <iostream>
#include <format>
#include <string>
template<typename T>
std::string convert(T input) {
return std::format("{}", input);
}
int main() {
std::cout << convert(1) << std::endl;
std::cout << convert(1.1e-3) << std::endl;
std::cout << convert("Hello Debao") << std::endl;
}
|
如果我们需要对个别类型特殊处理的话,可以使用if constexpr
(实例化有只有一个return):
| template<typename T>
std::string convert(T input) {
if constexpr (std::is_same_v<T, const char*> || std::is_same_v<T, std::string>)
return input;
else
return std::format("{}", input);
}
|
如下代码尽管工作,但会实例出两个return,要避免:
| template<typename T>
std::string convert(T input) {
if constexpr (std::is_same_v<T, const char*> || std::is_same_v<T, std::string>)
return input;
return std::format("{}", input);
}
|
如果不用if constexpr
,可使用函数特化(“传统”,就是冗余一些):
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | template<typename T>
std::string convert(T input) {
return std::format("{}", input);
}
template<>
std::string convert(const char* input) {
return input;
}
template<>
std::string convert(std::string input) {
return input;
}
|
以上用的C++20的内容。在C++14下,可以继续用重载方式;但是不能用下列方式(因为每个支路都要实例化,需要都有效才行):
| template<typename T>
std::string convert(T input) {
if (std::is_same<T, const char*>::value || std::is_same<T, std::string>::value) {
return input;
} else {
std::ostringstream oss;
oss << input;
return oss.str();
}
}
|
类模板(Class Templates)
例子1 - 简单的类模板
一个简单的类模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | #include <string>
#include <iostream>
#include <format>
template <typename T>
class Box {
public:
Box(T v) : value(v) {}
T get() const { return value; }
private:
T value;
};
int main() {
using namespace std::string_literals;
Box intBox(112);
Box strBox("1+1=10"s);
std::cout << std::format("{} {}\n", intBox.get(), strBox.get());
}
|
如果要加约束,可以用concept取代 typename位置:
| template <std::integral T>
class Box {
public:
Box(T v) : value(v) {}
T get() const { return value; }
private:
T value;
};
|
或者使用requires:
| template <typename T>
requires std::integral<T>
class Box {
public:
Box(T v) : value(v) {}
T get() const { return value; }
private:
T value;
};
|
或者
| template <typename T>
class Box {
public:
Box(T v) : value(v) {}
T get() const requires std::integral<T> { return value; }
private:
T value;
};
|
在C++20之前,使用 std::enable_if
这种SAFINE机制:
| template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>> // 使用 SFINAE
class Box {
public:
Box(T v) : value(v) {}
T get() const { return value; }
private:
T value;
};
|
或者,使用static_assert
这种静态断言:
| template <typename T>
class Box {
static_assert(std::is_integral_v<T>, "T must be integral type");
public:
Box(T v) : value(v) {}
T get() const { return value; }
private:
T value;
};
|
例子2 - 特化与 traits
类模板的特化和特征(traits)关系密切。
比如用于判定类型是否指针的 std::is_pointer
或 std::is_pointer_v
:
| #include <iostream>
#include <type_traits>
int main() {
std::cout << std::boolalpha;
std::cout << "int: " << std::is_pointer_v<int> << '\n';
std::cout << "int*: " << std::is_pointer_v<int*> << '\n';
std::cout << "double*: " << std::is_pointer_v<double*> << '\n';
return 0;
}
|
借助标准库通过特化可这么实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | #include <iostream>
#include <type_traits>
template<typename T>
struct is_pointer : std::false_type {};
template<typename T>
struct is_pointer<T*> : std::true_type {};
int main() {
std::cout << std::boolalpha;
std::cout << "int: " << is_pointer<int>::value << '\n'; // false
std::cout << "int*: " << is_pointer<int*>::value << '\n'; // true
std::cout << "double*: " << is_pointer<double*>::value << '\n'; // true
return 0;
}
|
而在C++11之前,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | #include <iostream>
template<typename T>
struct is_pointer {
static const bool value = false;
};
template<typename T>
struct is_pointer<T*> {
static const bool value = true;
};
int main() {
std::cout << std::boolalpha;
std::cout << "int: " << is_pointer<int>::value << '\n';
std::cout << "int*: " << is_pointer<int*>::value << '\n';
std::cout << "double*: " << is_pointer<double*>::value << '\n';
return 0;
}
|
非类型模板参数(Non-Type Template Parameters)
非类型模板参数(Non-Type Template Parameters,NTTP)的变化自C++11起没前面那么大。
- 类型参数:用 typename 或 class 声明的模板参数,如
template<typename T>
,T 表示一种类型,可以是 int、float、DbClass 等;
- 非类型参数:用类型限定的实际值作为参数,比如
template<int N>
中的 N 是一个非类型参数,表示一个整数值。
例子1 - 计算阶乘
涉及NTTP,这类计算的例子似乎挺常见:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | #include <iostream>
template<int N>
constexpr int factorial() {
if constexpr (N <= 1)
return 1;
else
return N * factorial<N - 1>();
}
int main() {
std::cout << "Factorial of 5 is: " << factorial<5>() << std::endl;
return 0;
}
|
上面用了 C++17支持的编译期 if constexpr
。但是这个小例子,可以不用它,不用模板:
| #include <iostream>
constexpr int factorial(int N) {
return (N <= 1) ? 1 : N * factorial(N - 1);
}
int main() {
std::cout << "Factorial of 5: " << factorial(5) << std::endl;
return 0;
}
|
里面用了 C++11时引入的 constexpr。而在C++11之前,只能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | #include <iostream>
template<int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
static const int value = 1;
};
int main() {
std::cout << "Factorial of 5: " << Factorial<5>::value << std::endl;
return 0;
}
|
例子2 - 数组大小
非类型参数可用于数组大小模板,这样它即使不存放这个参数也能用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | #include <iostream>
template<std::size_t N>
struct Array {
int data[N];
void print() const {
for (auto i = 0; i < N; ++i) {
std::cout << data[i] << " ";
}
std::cout << std::endl;
}
};
int main() {
Array<5> arr = {1, 2, 3, 4, 5};
arr.print();
}
|
Lambda表达式(Lambda Expressions)
Lambda变化挺快:
- C++11:基础 Lambda 表达式,不支持泛型参数和 constexpr。
- C++14:引入了泛型 Lambda(使用 auto 作为参数)。
- C++17:增加了 constexpr 支持。
- C++20:引入了 Lambda 模板和 concepts,增强了类型约束能力。
例子1 - 泛型
C++14支持如下泛型写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | #include <iostream>
using namespace std::literals;
int main() {
auto add = [](auto a, auto b) {
return a + b;
};
std::cout << add(1, 1) << std::endl;
std::cout << add(1.0, 1.0) << std::endl;
std::cout << add("1"s, "1"s) << std::endl;
return 0;
}
|
但是,add函数如下写法直到C++20才支持:
| auto add = []<typename T>(T a, T b) {
return a + b;
};
|
例子2 - 约束
1
2
3
4
5
6
7
8
9
10
11
12 | #include <iostream>
#include <concepts>
int main() {
auto add = [](std::integral auto a, std::integral auto b) {
return a + b;
};
std::cout << add(1, 1) << std::endl;
// std::cout << add(1.0, 1.0) << std::endl;
return 0;
}
|
或
| auto add = []<std::integral T>(T a, T b) {
return a + b;
};
|
或
| auto add = []<typename T>(T a, T b) requires std::integral<T> {
return a + b;
};
|
里面用到C++20的concept约束,如果在C++20之前的话,使用SFANE:
| auto add = [](auto a, auto b) -> std::enable_if_t<std::is_integral_v<decltype(a)>, decltype(a + b)> {
return a + b;
};
|
变参模板(Variadic Templates)
变参模板允许模板接受可变数量的模板参数。C++11 引入的这一特性。
C++17引入折叠表达式(Fold expressions),有四种写法(烧脑):
| ( pack op ... )
( ... op pack )
( pack op ... op init )
( init op ... op pack )
|
假定参数包E中有N个元素用E_下标
表示,以上四种写法对应:
- 一元右折叠(Unary right fold):
(E op ...)
展开为 (E_1 op (... op (E_{N-1} op E_N)))
- 一元左折叠(Unary left fold):
(... op E)
展开为 (((E_1 op E_2) op ...) op E_N)
- 二元右折叠(Binary right fold):
(E op ... op I)
展开为 (E_1 op (... op (E_{N−1} op (E_N op I))))
- 二元左折叠(Binary left fold):
(I op ... op E)
展开为 ((((I op E_1) op E_2) op ...) op E_N)
例子1 - 简单场景
| #include <iostream>
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
}
int main() {
print(1, 2, 3);
print("Hello", 42, 3.14);
}
|
其中使用了C++17引入的折叠表达式(Fold Expressions)。具体来说,(std::cout << ... << args)
是二元左折叠,它将参数依次从左到右进行操作,如 (std::cout << arg1) << arg2 << arg3 ...
如果不用折叠表达式,可以用递归来实现print
函数:
| template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
print(rest...);
} else {
std::cout << '\n';
}
}
|
但是if constexpr
也是C++17引入的,要在C++14中使用递归的话,需要类似下面这样:
| void print() {
std::cout << '\n';
}
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
print(rest...);
}
|
或者用逗号表达式展开
| template<typename T>
void printSingle(T arg) {
std::cout << arg;
}
template<typename... Args>
void print(Args... args) {
(printSingle(args), ...);
std::cout << '\n';
}
|
例子2 - 非类型参数
一个求和的例子,可以使用折叠表达式:
| #include <iostream>
template<int... Ns>
int sum() {
return (Ns + ...);
}
int main() {
std::cout << sum<1, 2, 3, 4>() << std::endl;
return 0;
}
|
或者使用if constexpr
和递归:
| template<int N, int... Ns>
int sum() {
if constexpr (0 == sizeof...(Ns))
return N;
else
return N + sum<Ns...>();
}
|
以上写法折叠表达式和if constexpr
都需要C++17。要在C++14中使用递归,需要:
| template<int N>
int sum() {
return N;
}
template<int N, int... Ns>
int sum() {
return N + sum<Ns...>();
}
|
参考
- https://en.cppreference.com/w/cpp/concepts
- https://github.com/AnthonyCalandra/modern-cpp-features
- https://www.cppstories.com/2016/02/notes-on-c-sfinae/
- https://zh.cppreference.com/w/cpp/language/fold