专业编程基础技术教程

网站首页 > 基础教程 正文

C++ 使用模板参数推导以简化和澄清

ccvgpt 2024-08-03 12:32:08 基础教程 41 ℃

模板参数推导是指模板函数的参数类型,或类模板构造函数(从 C++17 开始),足够清晰,编译器能够在不使用模板参数的情况下理解。这个特性有一定的规则,但大多是直观的。

如何做到这一点…

C++ 使用模板参数推导以简化和澄清

一般来说,当你使用具有明确兼容参数的模板时,模板参数推导会自动发生。让我们看一些例子。

在函数模板中,参数推导通常是这样的:

template<typename T>
const char * f(const T a) {
    return typeid(T).name();
}
int main() {
    cout << format("T is {}\n", f(47));
    cout << format("T is {}\n", f(47L));
    cout << format("T is {}\n", f(47.0));
    cout << format("T is {}\n", f("47"));
    cout << format("T is {}\n", f("47"s));
}

输出:

T is int
T is long
T is double
T is char const *
T is class std::basic_string<char...


因为类型很容易辨别,所以在函数调用中没有必要指定模板参数,如 f<int>(47)。编译器可以从参数中推导出 `<int>` 类型。

注意

上述输出显示了有意义的类型名称,大多数编译器会使用缩写,如 i 代表 int,PKc 代表 const char * 等。

这对于多个模板参数同样有效:

template<typename T1, typename T2>
string f(const T1 a, const T2 b) {
    return format("{} {}", typeid(T1).name(), typeid(T2).name());
}
int main() {
    cout << format("T1 T2: {}\n", f(47, 47L));
    cout << format("T1 T2: {}\n", f(47L, 47.0));
    cout << format("T1 T2: {}\n", f(47.0, "47"));
}


输出:

T1 T2: int long
T1 T2: long double
T1 T2: double char const *


这里编译器推导出了 T1 和 T2 的类型。

注意,类型必须与模板兼容。例如,你不能从字面量中获取引用:

template<typename T>
const char * f(const T& a) {
    return typeid(T).name();
}
int main() {
    int x{47};
    f(47);  // 这将不会编译
    f(x);   // 但这个会
}


从 C++17 开始,你还可以在类中使用模板参数推导。所以现在这将有效:

pair p(47, 47.0);     // 推导为 pair<int, double>
tuple t(9, 17, 2.5);  // 推导为 tuple<int, int, double>


这消除了对 std::make_pair() 和 std::make_tuple() 的需求,因为现在你可以在不明确指定模板参数的情况下直接初始化这些类。为了向后兼容,std::make_* 辅助函数将继续保持可用。

它是如何工作的…

让我们定义一个类,看看这是如何工作的:

template<typename T1, typename T2, typename T3>
class Thing {
    T1 v1{};
    T2 v2{};
    T3 v3{};
public:
    explicit Thing(T1 p1, T2 p2, T3 p3)
    : v1{p1}, v2{p2}, v3{p3} {}
    string print() {
        return format("{}, {}, {}\n",
                        typeid(v1).name(),
                        typeid(v2).name(),
                        typeid(v3).name()
        );
    }
};


这是一个具有三种类型和三个相应数据成员的模板类。它有一个 print() 函数,返回一个格式化字符串,包含三个类型名称。

没有模板参数推导,我将不得不像这样实例化此类的一个对象:

Things<int, double, string> thing1{1, 47.0, "three"}


现在我可以这样做:

Things thing1{1, 47.0, "three"}


这既更简单,又减少了出错的可能性。

当我在 thing1 对象上调用 print() 函数时,我得到这个结果:

cout << thing1.print();


输出:

int, double, char const *


当然,你的编译器可能会报告一些有效的相似内容。

在 C++17 之前,模板参数推导不适用于类,因此你需要一个辅助函数,它可能看起来像这样:

template<typename T1, typename T2, typename T3>
Things<T1, T2, T3> make_things(T1 p1, T2 p2, T3 p3) {
    return Things<T1, T2, T3>(p1, p2, p3);
}

auto thing1(make_things(1, 47.0, "three"));
cout << thing1.print();


输出:

int, double, char const *


STL 包括一些这样的辅助函数,如 make_pair() 和 make_tuple() 等。这些现在已过时,但将为了与旧代码兼容而保持。

还有更多…

考虑具有参数包的构造函数的情况:

template <typename T>
class Sum {
    T v{};
public:
    template <typename... Ts>
    Sum(Ts&& ... values) : v{ (values + ...) } {}
    const T& value() const { return v; }
};


注意构造函数中的折叠表达式(values + ...)。这是 C++17 的特性,它将运算符应用于参数包的所有成员。在这种情况下,它将 v 初始化为参数包的总和。

这个类的构造函数接受任意数量的参数,其中每个参数可能是不同的类。例如,我可以这样调用它:

Sum s1 { 1u, 2.0, 3, 4.0f };  // 无符号整数,双精度浮点数,整数,单精度浮点数
Sum s2 { "abc"s, "def" };     // std::string,C 字符串


当然,这不会编译。模板参数推导未能找到所有不同参数的共同类型。我们得到一个错误消息,大意是:

无法推导 'Sum' 的模板参数

我们可以使用模板推导指南来修复这个问题。推导指南是一种辅助模式,用于帮助编译器进行复杂推导。这是我们构造函数的指南:

template <typename... Ts>
Sum(Ts&& ... ts) -> Sum<std::common_type_t<Ts...>>;


这告诉编译器使用 std::common_type_t 特性,它尝试为包中的所有参数找到一个共同类型。现在我们的参数推导工作了,我们可以看到它确定了哪些类型:

Sum s1 { 1u, 2.0, 3, 4.0f };  // 无符号整数,双精度浮点数,整数,单精度浮点数
Sum s2 { "abc"s, "def" };     // std::string,C 字符串
auto v1 = s1.value();
auto v2 = s2.value();
cout << format("s1 is {} {}, s2 is {} {}",
                typeid(v1).name(), v1, typeid(v2).name(), v2);


输出:

s1 is double 10, s2 is class std::string abcdef

Tags:

最近发表
标签列表