Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
josuttis_nm_c20_the_complete_guide.pdf
Скачиваний:
44
Добавлен:
27.03.2023
Размер:
5.85 Mб
Скачать

676

Chapter 22: Small Improvements for Generic Programming

22.2Improvements for Aggregates in Generic Code

C++20 provides a couple of improvements for aggregates. For generic code we now have:

Using class template argument deduction (CTAD) for aggregates

Aggregates can be used as non-type template parameters (NTTP) The former is described in this section.

Note that there are other new features for aggregates:

Designated initializers (initial values for specific members) are (partially) supported

You can initialize aggregates with parentheses

The fixed definition of aggregates and the consequence for std::is_default_constructible<>

22.2.1 Class Template Argument Deduction (CTAD) for Aggregates

Since C++17, constructors can be used to deduce template parameters of class templates. For example:

template<typename T> class Type {

T value; public: Type(T val)

: value{val} {

}

...

};

 

Type<int> t1{42};

 

Type t2{42};

// deduces Type<int> since C++17

However, even for simple aggregates, a similar deduction according to the way objects are initialized was not supported:

template<typename T> struct Aggr {

T value;

};

 

Aggr<int> a1{42};

// OK

Aggr a2{42};

// ERROR before C++20

You had to provide a deduction guide:

template<typename T> struct Aggr {

T value;

};

22.2 Improvements for Aggregates in Generic Code

677

template<typename T> Aggr(T) -> Aggr<T>;

Aggr<int> a1{42};

// OK

Aggr a2{42};

// OK since C++17

Since C++20, there is no longer any need for a deduction guide, meaning that the following is enough:

template<typename T> struct Aggr {

T value;

};

 

Aggr<int> a1{42};

// OK

Aggr a2{42};

// OK since C++20 even without deduction guide

Note that this feature also works when using parentheses to initialize aggregates:

Aggr a3(42);

// OK since C++20 even without deduction guide

The deduction rules can be subtle, as the following example demonstrates:

template<typename T> struct S {

Tx;

Ty;

};

template<typename T> struct C {

S<T> s;

Tx;

Ty;

};

 

 

C c1 = {{1, 2}, 3, 4};

// OK, C<int> deduced

C c2 = {{1, 2}, 3};

// OK, C<int> deduced (y is 0)

C c3 = {{1, 2}, 3.3, 4.4};

// OK, C<double> deduced

C c4

= {{1, 2}, 3, 4.4};

// ERROR: int and double deduced for T

C c5

= {{1, 2}};

// ERROR: T only indirectly deduced

C c6

= {1, 2, 3};

// ERROR: don’t know how many values S<T> needs

C<int> c7 = {1, 2, 3};

// OK

678

Chapter 22: Small Improvements for Generic Programming

Note that class template argument deduction even works for aggregates with a variadic number of elements:

// aggregate with variadic number of base types: template<typename... T>

struct C : T... { };

struct Base1 { };

struct Base2 { };

//aggregate initialized with two elements of types Base1 and Base2:

C c1{Base1{}, Base2{}};

//aggregate initialized with three lambda elements:

C c2{[] { f1(); }, [] { f2(); }, [] { f3(); } };

22.3Conditional explicit

To disable implicit type conversions, constructors can be declared as explicit. However, for generic code, you might want to make the constructors explicit if and only if a type parameter has explicit constructors. That way, you can perfectly delegate type conversion support to a wrapper type.

The way to specify a conditional explicit is like specifying a conditional noexcept. Directly after

explicit, you can specify a Boolean compile-time expression in parentheses. Here is an example:

 

lang/wrapper.hpp

 

#include <type_traits> // for std::is_convertible_v<>

template<typename T> class Wrapper {

T value; public:

template<typename U> explicit(!std::is_convertible_v<U, T>) Wrapper(const U& val)

:value{val} {

 

}

};

...

 

22.3 Conditional explicit

679

The class template Wrapper<> that holds values of type T has a generic constructor, meaning that you could initialize the value with any type that is implicitly convertible to T. If there is no implicit conversion from U to T, the constructor is explicit.

As a consequence, you can initialize a Wrapper of a type only if there is an implicit type conversion enabled. For example:

lang/explicitwrapper.cpp

#include "wrapper.hpp" #include <string> #include <vector>

void printStringWrapper(Wrapper<std::string>) {

}

void printVectorWrapper(Wrapper<std::vector<std::string>>) {

}

int main()

 

 

{

 

 

// implicit conversion from string literal to string:

 

 

std::string s1{"hello"};

 

 

std::string s2 = "hello";

// OK

Wrapper<std::string> ws1{"hello"};

 

 

Wrapper<std::string> ws2 = "hello";

// OK

printStringWrapper("hello");

// OK

// NO implicit conversion from size to vector<string>:

 

 

std::vector<std::string> v1{42u};

 

 

std::vector<std::string> v2 = 42u;

// ERROR: explicit

Wrapper<std::vector<std::string>> wv1{42u};

 

 

Wrapper<std::vector<std::string>> wv2 = 4u2;

// ERROR: explicit

printVectorWrapper(42u);

// ERROR: explicit

 

 

}

 

 

To demonstrate the effect of a conditional explicit, we use a Wrapper<> for strings and for vectors of strings:

For type std::string, the constructor enables implicit conversions from string literals. Therefore, the constructor of type Wrapper<> is not explicit, which has the effect that we pass string literals to copy initialize a string wrapper or pass a string literal to a function that takes a string wrapper.2

For type std::vector<std::string>, the constructor that takes an unsigned size is declared as explicit in the C++ standard library. For that reason, std::is_convertible<> from a size to the vector is false and the Wrapper<> constructor becomes explicit. Therefore, we also cannot pass a size to initialize a wrapper of a vector of strings or pass a size to a function that takes that wrapper type.

2 Remember that implicit conversions are required for copy initialization (initialization with =) and parameter passing.