乐于分享
好东西不私藏

告别冗余:C++17 让你不再重复写模板参数

告别冗余:C++17 让你不再重复写模板参数

这篇文章将深入讲解 C++17 引入的类模板参数推导(Class Template Argument Deduction,简称 CTAD)

我们会从“为什么要引入”说起,结合代码示例,说明它的用法、原理、局限以及自定义推导指引。


一、C++17 之前的限制

在 C++11/14 中,虽然函数模板可以根据参数推导模板类型,但类模板不行。
声明一个类模板对象时,必须显式写出所有模板参数。

例如:

std::complex<double> c{5.13.3};   // 必须写 double
std::lock_guard<std::mutex> lg(mx)// 必须写 std::mutex
std::vector<int> v1{123};       // 必须写 int

这有时显得多余,因为从构造函数参数完全可以推导出类型。


二、C++17 的类模板参数推导(CTAD)

C++17 允许编译器从构造函数参数推导类模板参数

基本语法

std::complex c{5.13.3};   // 推导为 complex<double>
std::lock_guard lg{mx};     // 推导为 lock_guard<mutex>
std::vector v1{123};    // 推导为 vector<int>

注意:C++17 中建议使用花括号 {} 初始化,因为圆括号在某些情况下会有解析问题(比如与函数声明冲突)。


三、工作原理

CTAD 并不是魔法,它背后依赖的是隐式生成的推导指引(deduction guide)

对于像 std::complex<T> 这样的类,如果有构造函数:

template<classT>
classcomplex {

public:
complex(T re, T im);
};

编译器会自动生成一个隐式推导指引

template<class T>
complex(T, T) -> complex<T>
;

这告诉编译器:当你用两个相同类型的参数构造时,就推导出 complex<T>


四、需要显式指引的场景

有些类模板的构造函数参数不是直接给出模板参数类型,而是通过包装或转发时,隐式推导可能失败。

例子:std::vector 的迭代器区间构造

std::vector 有一个模板构造函数:

template<class InputIt>
vector(InputIt first, InputIt last)
;

如果没有显式推导指引,vector v(vi.begin(), vi.end()); 会推导成 vector<InputIt>,而不是 vector<int>

C++17 标准库已经为这种情况提供了显式推导指引:

template<class InputIt>
vector(InputIt, InputIt) -> vector<typename iterator_traits<InputIt>::value_type>
;

这样就能正确推导元素类型。


五、自定义推导指引

我们也可以为自定义类添加推导指引。

示例:包装器 Box<T>

template<typename T>
classBox {
public:
    Box(T value) : data(value) {}
    Box(T a, T b) : data(a + b) {}
private:
    T data;
};

// 推导指引
Box(constchar*) -> Box<std::string>;  // 将 C 字符串推导为 string

// 编译器会生成隐式指引:
// Box(T) -> Box<T>;
// Box(T, T) -> Box<T>;

使用时:

Box b1(42);          // Box<int>
Box b2(3.142.71);  // Box<double>
Box b3("hello");     // Box<std::string>(由显式指引决定)

如果没有上面的 Box(const char*) -> Box<std::string>Box b3("hello") 会推导成 Box<const char*>


六、CTAD 的限制与注意事项

  1. 不能用于无构造函数的类
    如果有 = delete 或 private 构造函数且无其他可推导来源,推导失败。

  2. 不能推导非类型模板参数
    例如 std::array<int, 3>,C++17 的 CTAD 只做类型推导,不推导大小。

    std::array arr{123};  // 错误:无法推导大小

    C++20 改进了这一点,但 C++17 不行。

  3. 不能用于继承构造函数的类

  4. 与函数模板的默认参数类似,不支持部分推导
    要么全推导,要么全写出(除非提供指引)。

  5. 圆括号与花括号的区别

    std::vectorv1(1020);   // 可能被解释为函数声明,或推导失败?
    std::vector v2{1020};   // 明确地 vector<int>,两个元素 10 和 20

    一般来说,CTAD 与 {} 配合最安全。


七、CTAD 与聚合体(C++20 增强,但提及)

C++17 的 CTAD 对聚合体支持不完整,需要显式指引。
C++20 允许 vector v = {1, 2, 3}; 这种写法,但那是聚合体推导的扩展。


八、实际应用总结

场景
C++17 前
C++17 使用 CTAD
complexcomplex<double>complex{1.0, 2.0}
lock_guardlock_guard<mutex>lock_guard{mtx}
vectorvector<int>vector{1, 2, 3}
自定义类
不能省略类型
可自动推导或加指引

九、完整代码示例

#include<iostream>
#include<complex>
#include<mutex>
#include<vector>

template<typename T>
classMyWrapper {
public:
    MyWrapper(T x) : val(x) {}
voidprint()std::cout << val << std::endl; }
private:
    T val;
};

// 自定义推导指引:将 const char* 推导为 std::string
MyWrapper(constchar*) -> MyWrapper<std::string>;

intmain(){
std::complex c{3.04.0};   // complex<double>
std::mutex mx;
std::lock_guard lg{mx};     // lock_guard<mutex>

std::vector v1{123};    // vector<int>
std::vector v2{"hello""world"}; // vector<const char*>

MyWrapper w1(123);            // MyWrapper<int>
MyWrapper w2("hello");        // MyWrapper<std::string>

    w1.print();
    w2.print();

return0;
}

总结

C++17 的类模板参数推导(CTAD):

  • 简化了代码,去除了冗余的模板参数指定
  • 依赖隐式或显式的推导指引
  • 提升代码可读性,尤其与 STL 容器、智能指针、锁等一起使用时
  • 虽有局限(非类型参数、聚合体等),但在绝大多数场景下非常实用

这是 C++17 提高生产力最重要的特性之一。