1+1=10

记记笔记,放松一下...

C++泛型编程小记

世界变化太快,C++的各种写法,转眼都快看不懂了。通过傻瓜例子简单梳理些 C++ 基础内容

什么是泛型编程(Generic Programming)?

泛型编程是一种编程范式,其核心思想是编写与类型无关的代码,从而提高代码的复用性和灵活性。

几乎所有现代编程语言都提供了泛型编程的支持,某些语言(如 Java、C#)中称为“泛型(Generics)”的机制,通常通过类型擦除或运行时检查来实现;某些语言(如 Haskell)使用“参数多态(Parametric Polymorphism)”来描述泛型编程。但在 C++ 中,泛型编程实现被称为模板(Templates),是基于编译期实例化的。C++标准库的大量组件(如容器、算法、智能指针等)都是使用模板实现的。

C++ 模板主要有两种形式:

  • 函数模板:定义与类型无关的通用函数。
  • 类模板:定义与类型无关的通用类。

C++ 模板具有下列特色:

  • 编译期展开:模板在编译期被实例化为具体类型的代码,从而提高性能。
  • 模板特化:允许为某些类型提供特定实现,包括完全特化和部分特化。
  • 模板元编程:允许在编译期进行复杂的计算和类型推导。
  • 可变参数模板:允许模板接收任意数量的参数。
  • 概念(Concepts):C++20 引入的特性,用于约束模板参数的类型和行为。

模板元编程和编译期展开,超出了泛型编程的范畴。它们用来在编译期优化代码或进行复杂计算,增加灵活性的同时,也造就了C++模板系统的高复杂性。

Concepts

concept 是一个编译期的布尔值表达式,用来描述类型的要求。你可以使用 template 语法来定义 concept。

1
2
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 中使用,用于检查类型是否满足某些操作或行为:

1
2
3
4
5
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具备推导返回值类型能力):

1
2
3
4
template<typename T, typename U>
auto add(T a, U b) {
    return a + b;
}

而到了C++11,单独auto也不行了,它只是一个占位符,需要使用 decltype来推导返回值类型:

1
2
3
4
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函数可以写成这样:

1
2
3
4
template<std::integral T, std::integral U>
auto add(T a, U b) {
    return a + b;
}

也可以这样:

1
2
3
4
5
template<typename T, typename U>
requires std::integral<T> && std::integral<U> 
auto add(T a, U b) {
    return a + b;
}

还可以这样:

1
2
3
4
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

1
2
3
4
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 决定了是否会实例化。

如果不使用上面的占位符参数,可以用返回值做文章:

1
2
3
4
5
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

1
2
3
4
5
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_tstd::is_integral_v,只能:

1
2
3
4
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;
}

1
2
3
4
5
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方式写出来:

1
2
3
4
auto add(std::integral auto a, std::integral auto b)
    requires std::same_as<decltype(a), decltype(b)> {
    return a + b;
}

1
2
3
4
5
template<std::integral T, std::integral U>
requires std::same_as<T, U>
auto add(T a, U b) {
    return a + b;
}

1
2
3
4
template<std::integral T, std::integral U>
auto add(T a, U b) requires std::same_as<T, U> {
    return a + b;
}

尽管不如常规单模板参数写法简单:

1
2
3
4
template<std::integral T>
T add(T a, T b) {
    return a + b;
}

但比下面写法舒服:

1
2
3
4
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):

1
2
3
4
5
6
7
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,要避免:

1
2
3
4
5
6
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下,可以继续用重载方式;但是不能用下列方式(因为每个支路都要实例化,需要都有效才行):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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位置:

1
2
3
4
5
6
7
8
template <std::integral T>
class Box {
public:
    Box(T v) : value(v) {}
    T get() const { return value; }
private:
    T value;
};

或者使用requires:

1
2
3
4
5
6
7
8
9
template <typename T>
    requires std::integral<T>
class Box {
public:
    Box(T v) : value(v) {}
    T get() const { return value; }
private:
    T value;
};

或者

1
2
3
4
5
6
7
8
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机制:

1
2
3
4
5
6
7
8
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 这种静态断言:

1
2
3
4
5
6
7
8
9
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_pointerstd::is_pointer_v

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#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。但是这个小例子,可以不用它,不用模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#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才支持:

1
2
3
    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;
}

1
2
3
    auto add = []<std::integral T>(T a, T b) {
        return a + b;
        };

1
2
3
    auto add = []<typename T>(T a, T b) requires std::integral<T> {
        return a + b;
        };

里面用到C++20的concept约束,如果在C++20之前的话,使用SFANE:

1
2
3
    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),有四种写法(烧脑):

1
2
3
4
( 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 - 简单场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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函数:

1
2
3
4
5
6
7
8
9
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中使用递归的话,需要类似下面这样:

1
2
3
4
5
6
7
8
9
void print() {
    std::cout << '\n';
}

template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first;
    print(rest...);
}

或者用逗号表达式展开

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void printSingle(T arg) {
    std::cout << arg;
}

template<typename... Args>
void print(Args... args) {
    (printSingle(args), ...);
    std::cout << '\n';
}

例子2 - 非类型参数

一个求和的例子,可以使用折叠表达式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

template<int... Ns>
int sum() {
    return (Ns + ...);
}

int main() {
    std::cout << sum<1, 2, 3, 4>() << std::endl;
    return 0;
}

或者使用if constexpr 和递归:

1
2
3
4
5
6
7
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中使用递归,需要:

1
2
3
4
5
6
7
8
9
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

C++