Class template argument deduction (CTAD) (since C++17)

history

在C++98中,使用类模板时,即使从使用中可以明显看出它们的类型,也总是必须提供模板参数:

std::pair<int, std::string> p(1729, "taxicab");

随着C ++ 11中auto的引入,情况发生了一些变化。可以使用(预先存在的)辅助函数make_pair创建变量,从而避免重复类型:

#include 
#include 
#include 
#include 

int main() {
    std::pair p(1729, "taxicab");// deduces to std::pair p(1729, "taxicab");
    static_assert(std::is_same_v<decltype(p), std::pair<int, const char *>>);
   
    //std::tuple t(4, 3, 2.5); // same as auto t = std::make_tuple(4, 3, 2.5);
	auto t = std::make_pair(4, 2.5);
	static_assert(std::is_same_v<decltype(t), std::pair<int, double>>);
    
    auto tt = std::make_tuple(4, 3, 2.5);
	static_assert(std::is_same_v<decltype(tt), std::tuple<int, int, double>>);
	
    std::less l;             // same as std::less l;
}

但是,这依赖于make_pair函数模板的存在,因此,如果您希望为自己的类模板提供类似的功能,则必须确保可以使用辅助函数。使用语言规则只是一种习惯用法,该语言规则允许在调用函数模板时推导出模板参数。

C++17 code using CTAD

正如c++委员会论文P0091R3在摘要中指出的那样:“如果构造函数可以像我们从其他函数和方法中所期望的那样推导出其模板参数,则……”这几乎就是CTAD所做的。

由于CTAD现在是一种标准语言功能,因此它可以用于任何现有的类模板,而无需进行任何其他更改。

// Existing C++ class
template <typename T>
struct point
{
  T x;
  T y;
};

int main()
{
  // New C++17 use
  point pt{0L,0L};
}

报错信息:

<source> error: cannot deduce template arguments of 'point', as it has no viable deduction guides

潜在问题

首先,请注意,在C ++ 17中添加CTAD不会破坏任何现有代码,因为它仅使某些以前格式错误的代码变为有效。

但是,在争用代码库并删除所有对变量声明的类模板参数的显式指定之前,有一些情况可能会很麻烦。

首先,只有在目标类型的构造函数使用模板参数的情况下,推导过程才有效。 该过程无法从参数神奇地猜测类型。因此,例如,CTAD对于以下类没有用,因为模板参数不是构造函数签名的一部分:

template <typename T>
class collection
{
	public:
    	collection(std::size_t size);
    // ...
};

当您希望调用的构造函数使用从模板参数派生的类型时,这也可能是一个问题,因为默认情况下无法通过“向后”推论基础模板参数。

例如,如果您希望从一对迭代器构造一个向量,然后调用构造器:

template<class InputIterator>
vector(InputIterator first, InputIterator last,const Allocator& = Allocator());

直接执行此操作存在问题,因为目标集合的模板参数类型隐含在提供的迭代器的value_type中。 (我们将在下面找到一种解决方法。)

其次,如果有多个构造函数,则所需的构造函数可能不是推论将找到的那个。 如果您尝试将CTAD与C ++ 17之前编写的类一起使用,则可能会出现问题,因为那时不需要在可用的构造函数中进行设计。即使编写类的方法的微小细节也可能使CTAD无法操作。

第三,在某些情况下,由于其他原因,您确实希望使用辅助函数来创建类的实例。标准库中一个明显的示例是std :: make_shared。正如Scott Meyer的在第21项中指出的那样:“建议使用std :: make_uniquestd :: make_shared来直接使用new。”

 #include 
  int main()
  {
    std::shared_ptr<int> p(new int(10));
    auto q{std::make_shared<int>(10)};
    std::shared_ptr r(new int(10)); // C++17
  }

pr的构造在类型是显式还是推导类型上有所不同,但是在两种情况下,共享指针都需要分配一条额外的数据来管理共享对象。在构造q时,可以使用一次分配创建目标对象和关联的管理结构。

帮助编译器选择合适的构造函数

类模板自变量推论的措词包括提供明确的推论指南的选项。

语法与函数模板的语法相似,区别在于声明是针对类似构造函数的实体,没有返回类型。

例如,正如我们在上面针对Vector的情况所看到的那样,该语言不会为带有一对迭代器的矢量构造函数推断出模板参数。下方示例代码中的推导指南已添加到标准库P0433R2中。

 template<class InputIterator, 
 		  class Allocator=allocator<typename iterator_traits<InputIterator>::value_type>>
vector(InputIterator, InputIterator, Allocator = Allocator()) 
	-> vector<typename iterator_traits<InputIterator>::value_type, Allocator>;

下方代码省略了(默认的)分配器参数以便于理解。

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

当看到构造函数调用带有一对迭代器时,这会指示编译器使用迭代器类型的value_type构造向量,例如:

 void foo(std::set<int> const &c)
  {
    std::vector v(c.begin(), c.end());
    // ...
  }

其中 v将按预期推导为std :: vector

(请注意,这比使用完整语法可用的选项范围更具限制性,因为在这种情况下,要构造的向量通常可以是可以转换为int的任何类型。)

帮忙阻止不当的选择

有时候,您可能会禁止在类中使用CTAD,例如,当选择的构造函数不太可能是用户期望的构造函数时。

消除使用类模板参数推论的可能性的一种方法是,将构造函数更改为采用从模板参数派生的类型。

template <typename T>
struct no_ctad { using type_t = T; };

template <typename T>
using no_ctad_t = typename no_ctad<T>::type_t;

template <typename T>
class test
{
public:
  test(no_ctad_t<T>);
};

int main()
{
   test t(1); // Error
}

不允许编译器“向上游工作”并推断可能的T值(即int),该值会使no_ctad_t 与提供的参数匹配。此规则已应用于函数调用中常规模板参数的推导。

(注意:Timur Doumler提出了标准化类似于no_ctad的类–此用例是具有启发性的示例之一)

P0091R4中详细介绍的第二种方法是扩展已删除功能的使用(“ = delete”),以也允许已删除的演绎指南。这尚未被工作文件采纳,但是Evolution工作组已经批准了该方向。

Primary template and explicit specialisations

类模板参数推导适用于主模板。如果此模板的显式专门化定义了不同的构造函数,则CTAD将找不到这些构造函数。

template <typename T>
class myclass {};

template <>
class myclass<int>
{
public:
   myclass(int);
   // ...
};

int main()
{
   myclass m(1); // Error: primary template only
                 // has a default ctor
}
		

未来发展方向

最初提出CTAD时,建议您提供一些模板参数,并推论其他参数。当前工作文件中并未对此进行介绍,因为从“全有或全无”的方法开始,其中出现混淆或歧义的机会较少的方法,并考虑如果有的话,考虑将来的扩展,这是最安全的方法。

编译器支持

CTAD是C ++ 17的一部分,因此最终将由任何支持当前C ++标准的编译器提供。

但是,在撰写本文时(2017年底),并非所有主流编译器都已发布实现该语言这一部分的版本。

因此,例如,尽管gcc 7.1和clang 5.0均支持该功能,但最新版本的MSVC却没有[ C ++ 17 progress ],英特尔的编译器也没有。当您阅读本文时,状态当然可能已更改。

因此,您在代码中使用类模板参数推论的能力将取决于您的项目针对的是哪些编译器以及这些版本的编译器。

摘要

类模板自变量的推导不允许您编写任何尚无法编写的新代码,尽管语法略有不同。

但是,语法的减少确实减轻了代码阅读者的负担,并且还确保了由于推导了目标类型,因此如果所提供的自变量的类型发生更改,则代码会自动更改。

我认为CTAD是减少认知开销和提高可读性的有用技术。

参考文献

  • C++17 Progress in VS 2017 15.5 and 15.6

  • [P0091R3] http://wg21.link/p0091r3

  • [P0091R4] http://wg21.link/p0091r4

  • [P0433R2] http://wg21.link/p0433r2

  • https://accu.org/index.php/journals/2465

  • https://devblogs.microsoft.com/cppblog/how-to-use-class-template-argument-deduction/

  • https://arne-mertz.de/2017/06/class-template-argument-deduction/

你可能感兴趣的:(C/C++)