c++模板类_类模板参数推导的注意事项(keng)

译自 https://medium.com/@barryrevzin/quirks-in-class-template-argument-deduction-ad08af42ebd6

在C++17之前,模板推导(基本上)只能用于两种场合:函数模板的参数推导和变量类型/函数返回类型中的auto推导。过去无法推导类模板的模板参数。

结果就是:使用类模板的时候,只能(1)显式指定模板参数,或(2)写一个make_*辅助函数来做参数推导。对于(1),要么是重复劳动/容易出错(如果模板参数就是提供给构造函数的参数的类型),要么根本是不可能的(如果参数是lambda)。对于(2),需要知道辅助函数到底叫什么……辅助函数的名字并不总是符合make_*的模式。 标准库有make_pairmake_tuplemake_move_iterator等符合模式的,但也有inserterback_inserter这样不符合模式的。

类模板参数推导改变了这种情况,它允许根据类模板的构造函数或者根据推导指引(deduction guide)来推导类模板的参数。于是我们可以写出这样的代码:

pair p(1, 2.0);     // pair<int, double>
tuple t(1, 2, 3.0); // tuple<int, int, double>

template<class Func>
class Foo() { 
public: 
    Foo(Func f) : func(f) {} 
    void operator()(int i) const { 
      std::cout << "Calling with " << i << endl;
      f(i); 
    } 
private: 
    Func func; 
};
for_each(vi.begin(), vi.end(), Foo([&](int i){...})); // Foo<some_lambda_type>

不需要显式指定类型。不需要make_*,即使参数是lambda。


然而,类模板参数推导(以下简称CTAD)有两个值得注意的地方(坑)。

第一个地方是,头一次出现了“两个变量声明看起来声明同一个类型,实际上声明不同类型”的情况(←_←其实不是头一次)。

// 都使用auto,但我们并不期待它们对应同一个类型
auto a = 1;
auto b = 2.0;

// 都使用std::pair,它看上去像一个类型但实际上却不是
// 两个声明对应不同的类型
std::pair c(1, 2);
std::pair d(1, 2.0);

我们用auto的时候,都知道auto不是一个类型。但是我们用类模板的名字的时候,需要停下来想一下。 当然,对于std::pair,明显它不是类型——大家都知道它是类模板。但如果是自定义的类型,可能就不那么明显了。在上面的例子中,cd看上去都是std::pair 类型的对象——因此具有相同类型。但实际上它们分别是std::pair<int,int>std::pair<int,double> 类型的对象。

在C++20引入的Concepts有同样的问题。实际上YAACD paper(《另一种有制约声明的方法》)把CTAD作为支持Concept name = ...的原因:

在变量声明中,省略 auto看来也是合理的: Constraint x = f2();
特别要注意的是,我们已经有一种语法可以进行(部分)推导,但是在语法中没有明确体现推导: std::tuple x = foo();

这种“使用看上去像类型但实际上不是类型的占位符”的问题不会消失。恰恰相反,它会变得更加普遍。所以只要牢记这个问题就好了。


第二个注意事项(坑),对我来说,是一个更大的问题。它是Concepts和CTAD在意义上的不同之处,来自于CTAD试图解决的问题。

在相关提案中,加入CTAD的动机可以概括成:我想构建一个类模板的实例(specialization),而不必显式指定模板参数——只要自动推导,不需要我写辅助工具或者查看这些参数是什么。也就是说,我想构建东西。

加入Concepts的动机更广,但对于有制约的变量声明(constrained variable declaration)而言,动机是:我想构建一个对象,其类型我不关心,但我想要表达对这个类型的一系列要求,而不是直接使用auto 完事。也就是说,我仍然在使用已有的类型,只是加上了一个标注

至少我(←_←本文的原作者)是这么想的。

看起来这两个想法不冲突,但实际上它们就是冲突的。看起来我们不需要在两者之间做出选择,但实际上需要。最近的Twitter讨论串反映了这种冲突:

77b9de06819da509d48093a315ba37f8.png

8dbcc2aa6bbf2e0a1fe1d8bf38b4adb3.png

288ead01ee2a2ae55c3fa1442dc43ed6.png

不要停下来啊,JF。

问题归结为:这段代码到底做了什么:

std::tuple<int> foo();

std::tuple x = foo();
auto y = foo();

声明变量x的意图是什么?我们是在构建新东西(CTAD的目标)还是把std::tuple作为一个标注以确保xstd::tuple而不是其他东西(Concepts的目标)?

STL指出大多数程序员期望xy具有相同的含义。 但这样的标注并非是CTAD的目标。CTAD是关于构建新东西的——这表示虽然y显然是std::tuple<int>, 但x应该是std::tuple<std::tuple<int>>。毕竟,这就是我们所要求的。我们根据参数构建了一个新的类模板实例(class template specialization)。

在这个例子中,冲突表现得更加明显:

// The tuple case
std::tuple a(1);    // unquestionably, tuple<int>

std::tuple b(a, a); // unquestionably, tuple<tuple<int>, tuple<int>>
std::tuple c(a);    // ??

// The vector case
std::vector x{1};    // unquestionably, vector<int>

std::vector y{x, x}; // unquestionably, vector<vector<int>>
std::vector z{x}; // ??

这就是Casey所指出的。ctuple<int>还是tuple<tuple<int>>zvector<int>还是vector<vector<int>>

现在,如果我们将CTAD用于复制,则复制优先。 这表示单参和多参实际上遵循不同的规则。现在,ctuple<int>zvector<int>。二者都只是复制构造自对应的参数。

换言之,如Casey所说,tuple(args...)的类型不仅取决于参数数量,还取决于参数类型。也就是说:

  • 如果sizeof...(args) != 1tuple<decay_t<decltype(args)>...>
  • 否则,如果arg0不是 tuple的实例:tuple<decay_t<decltype(arg0)>>
  • 否则,decay_t<decltype(arg0)>

这显然不简单。(←_←现实中的tuple有5个推导指引,比这里列出的更复杂)

我(←_←本文的原作者)认为这是一个不幸和不必要的冲突——尤其是考虑到Concepts即将到来。Concepts将使我们能够轻松区分两种情况:

template <typename T, template <typename...> class Z>
concept Specializes = ...;

// The tuple case
tuple a(1);              // unquestionably, tuple<int>

tuple b(a, a);           // unquestionably, tuple<tuple<int>, tuple<int>>
tuple c(a);              // tuple<tuple<int>>
Specializes<tuple> d(a); // tuple<int>

// The vector case
vector x{1};              // unquestionably, vector<int>

vector y{x, x};           // unquestionably, vector<vector<int>>
vector z{x};              // vector<vector<int>>
Specializes<vector> w{x}; // vector<int>

这样,我们就能使每种语言特性做其最擅长的事:CTAD构建新东西,Concepts限制已有的东西。


但这就是现有的规则,因此记住这些坑是很重要的。尤其是第二个坑——这意味着在泛型代码中使用CTAD时需要非常小心:

template <typename... Ts>
auto make_vector(Ts... elems) {
    std::vector v{elems...};
    assert(v.size() == sizeof...(elems)); // right??
    return v;
}

auto a = make_vector(1, 2, 3); // ok
auto b = make_vector(1);       // ok
auto c = make_vector(a, b);    // ok
auto d = make_vector(c); // assert fires

(←_←虽然译者觉得不会有人傻到这么写——即使没有文中提到的坑,这写法也有问题,比如make_vector(1u, 2, std::allocator<int>{});就能造成assertion failure。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值