テンプレート引数の推論のようなテンプレート属性
より前は
C++17
、クラス テンプレートのすべてのパラメーターを明示的に指定する必要がありました。たとえば、次の場合は省略できません
double
。
std::complex<double> c{
5.1, 3.3};
次のコードの 2 番目を省略することもできませんstd::mutex
。
std::mutex mx;
std::lock_guard<std::mutex> lg(mx);
それ以来C++17
、クラス テンプレート パラメーターを指定する必要があるという制限が緩和されました。クラス テンプレート引数推論(CTAD)を使用すると、コンパイラが初期値からすべてのテンプレート引数を推論できる限り、引数を指定しないままにすることができます。
例えば:
- 次のように宣言できるようになりました。
std::complex c{
5.1, 3.3}; // OK:推导出std::complex<double>
ただし、c++11
次のエラーが報告されます
error: missing template arguments before 'c'
7 | std::complex c{
5.1, 3.3};
- 次のように書くことができるようになりました。
std::mutex mx;
std::lock_guard lg{
mx}; // OK:推导出std::lock_guard<std::mutex>
c++11
次のエラーが報告されます
<source>:9:21: error: missing template arguments before 'lg'
9 | std::lock_guard lg(mx);
|
- コンテナに要素の型を推定させることもできるようになりました。
std::vector v1 {
1, 2, 3}; // OK:推导出std::vector<int>
std::vector v2 {
"hello", "world"}; // OK:推导出std::vector<const char*>
上記 3 つの例の前処理コードは次のとおりです。
std::complex<double> c = std::complex<double>{
5.0999999999999996, 3.2999999999999998};
std::mutex mx = std::mutex();
std::lock_guard<std::mutex> lg = std::lock_guard<std::mutex>(mx);
std::vector<int, std::allocator<int> > v1 = std::vector<int, std::allocator<int> >{
std::initializer_list<int>{
1, 2, 3}, std::allocator<int>()};
std::vector<const char *, std::allocator<const char *> > v2 = std::vector<const char *, std::allocator<const char *> >{
std::initializer_list<const char *>{
"hello", "world"}, std::allocator<const char *>()};
クラステンプレート引数推論の使用
クラス テンプレート引数の推定は、すべてのテンプレート パラメーターを初期値から推定できる限り使用できます。演繹プロセスは、(初期化が有効であることが保証されている限り)すべての形式の初期化をサポートします。
std::complex c1{
1.1, 2.2}; // 推导出std::complex<double>
std::complex c2(2.2, 3.3); // 推导出std::complex<double>
std::complex c3 = 3.3; // 推导出std::complex<double>
std::complex c4 = {
4.4}; // 推导出std::complex<double>
c++17
前処理コードは次のとおりです。
std::complex<double> c1 = std::complex<double>{
1.1000000000000001, 2.2000000000000002};
std::complex<double> c2 = std::complex<double>(2.2000000000000002, 3.2999999999999998);
std::complex<double> c3 = std::complex<double>(3.2999999999999998, 0.0);
std::complex<double> c4 = std::complex<double>{
4.4000000000000004, 0.0};
テンプレート パラメータの初期化と推定に必要なパラメータは 1 つstd::complex
だけであるため、次のようになります。T
namespace std {
template<typename T>
class complex {
constexpr complex(const T&re = T(), const T& im = T());
...
}
};
したがってc3
、 と はc4
正しく初期化できます。次のようなステートメントの場合:
std::complex c1{
1.1, 2.2};
コンパイラはコンストラクターを見つけてconstexpr complex(const T& re = T(), const T& im = T());
呼び出します。両方のパラメーターがdouble
型であるため、コンパイラーはT
それを推定しdouble
、次のコードを生成します。
complex<double>::complex(const double& re = double(), const double& im = double());
推論中、テンプレート引数は明確でなければならないことに注意してください。つまり、次の初期化コードはコンパイルできません。
std::complex c5{
5, 3.3}; // ERROR:尝试将T推导为int和double
通常のテンプレートと同様に、テンプレート パラメーターを推定するときに暗黙的な型変換は使用されません。可変個引数テンプレートに対してクラス テンプレートの引数推定を使用することもできます。たとえば、次のように定義された の場合std::tuple
:
namespace std {
template<typename... Types>
class tuple {
public:
constexpr tuple(const Types&...);
...
};
};
次のように宣言されます。
std::tuple t{
42, 'x', nullptr};
型は推定されstd::tuple<int, char, std::nullptr_t>
、前処理コードは次のとおりです。
std::tuple<int, char, std::nullptr_t> t = std::tuple<int, char, std::nullptr_t>{
42, 'x', nullptr};
型以外のテンプレート パラメータを推定することもできます。たとえば、渡されたパラメータに従って、配列の要素タイプと要素数の両方を推定できます。
#include <iostream>
template <typename T, int SZ>
class MyClass {
public:
MyClass(T (&)[SZ]) {
}
};
int main() {
MyClass mc("hello"); /* // 推导出T为const char,SZ为6*/}
ここでは、受信文字列リテラルが 6 文字であるため、SZ
次のように推定します。6
前処理コードは次のとおりです。
#include <iostream>
template<typename T, int SZ>
class MyClass
{
public:
inline MyClass(T (&)[SZ])
{
}
};
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class MyClass<const char, 6>
{
public:
inline MyClass(const char (&)[6])
{
}
};
#endif
/* First instantiated from: insights.cpp:9 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
MyClass(const char (&)[6]) -> MyClass<const char, 6>;
#endif
int main()
{
MyClass<const char, 6> mc = MyClass<const char, 6>("hello");
return 0;
}
オーバーロードを実装するための基本クラスとして使用されるものを推測したりlambda
、auto
テンプレート パラメーターを推測したりすることもできます。
デフォルトではコピーによって派生
クラス テンプレート パラメーター推定プロセスは、まず copy によって初期化を試みます。たとえば、最初に単一の要素を初期化しますstd::vector
。
std::vector v1{
42}; // 一个元素的vector<int>
前処理コードは次のとおりです。
std::vector<int, std::allocator<int> > v1 = std::vector<int, std::allocator<int> >{
std::initializer_list<int>{
42}, std::allocator<int>()};
std::vector v2{v1}; // v2也是一个std::vector<int>
次に、このベクトルを使用して別のベクトルを初期化します。これは、要素が 1 つだけあるベクトルを作成するのではなく、コピーを作成すると解釈されますvector<vector<int>>
。
このルールは、すべての形式の初期化に適用されます。
std::vector v2{
v1}; // v2也是vector<int>
std::vector v3(v1); // v3也是vector<int>
std::vector v4 = {
v1}; // v4也是vector<int>
auto v5 = std::vector{
v1}; // v5也是vector<int>
前処理コードは次のとおりです。
std::vector<int, std::allocator<int> > v2 = std::vector<int, std::allocator<int> >{
v1};
std::vector<int, std::allocator<int> > v3 = std::vector<int, std::allocator<int> >(v1);
std::vector<int, std::allocator<int> > v4 = std::vector<int, std::allocator<int> >{
v1};
std::vector<int, std::allocator<int> > v5 = std::vector<int, std::allocator<int> >{
v1};
これは、中括弧の初期化では常にリスト内の引数を要素として受け取るというルールの例外であることに注意してください。vector
1 つのみの初期化子列を渡して別の を初期化すると、渡されたもののコピーがvector
取得されます。vector
ただし、複数の要素の初期値列で初期化された場合、受信パラメータは要素として使用され、その型はテンプレート パラメータとして推定されます (この場合、コピーの作成として解釈できないため)。
std::vector vv{
v1, v2}; // vv是一个vector<vector<int>>
前処理コードは次のとおりです。
std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > > vv = std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > >{
std::initializer_list<std::vector<int, std::allocator<int> > >{
std::vector<int, std::allocator<int> >(v1), std::vector<int, std::allocator<int> >(v2)}, std::allocator<std::vector<int, std::allocator<int> > >()};
ここで、可変長引数テンプレートに対してクラス テンプレートの引数推定を使用すると何が起こるかという疑問が生じます。
template<typename... Args>
auto make_vector(const Args&... elems) {
return std::vector{
elem...};
}
std::vector<int> v{
1, 2, 3};
auto x1 = make_vector(v, v); // vector<vector<int>>
auto x2 = make_vector(v); // vector<int>还是vector<vector<int>>?
現在、コンパイラが異なれば動作も異なりますが、この問題はまだ議論中です。
派生lambda
型
クラス テンプレートの引数演繹を使用すると、lambda
型 (具体的には、lambda
生成されたクロージャの型) をテンプレート パラメーターとして使用してクラス テンプレートをインスタンス化できます。たとえば、任意のコールバック関数をラップし、呼び出し数をカウントするためのジェネリック クラスを提供できます。
#include <utility> // for std::forward()
template<typename CB>
class CountCalls
{
private:
CB callback; // 要调用的回调函数
long calls = 0; // 调用的次数
public:
CountCalls(CB cb) : callback(cb) {
}
template<typename... Args>
decltype(auto) operator() (Args&&... args) {
++calls;
return callback(std::forward<Args>(args)...);
}
long count() const {
return calls;
}
};
ここで、コンストラクターはコールバック関数を取得し、初期化時と同様にパラメーターの型が推定されるようにそれをラップしますCB
。たとえば、lambda
パラメータとして を使用してオブジェクトを初期化できます。
CountCalls sc{
[](auto x, auto y) {
return x > y; }};
これは、sc
照合基準のタイプが と推定されることを意味しますCountCalls<TypeOfTheLambda>
。前処理されたコンテンツは です CountCalls<__lambda_26_19> sc = CountCalls<__lambda_26_19>{__lambda_26_19{}};
。このようにして、並べ替え基準が呼び出された回数をカウントできます。
std::vector v{
5,4,3,2,1};
std::sort(v.begin(), v.end(), // 排序区间
std::ref(sc)); // 排序准则
std::cout << "sorted with " << sc.count() << " calls\n" <<v.at(0);
前処理コードは次のとおりです。
{
public:
template<class type_parameter_0_0, class type_parameter_0_1>
inline /*constexpr */ auto operator()(type_parameter_0_0 x, type_parameter_0_1 y) const
{
return x < y;
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ bool operator()<int, int>(int x, int y) const
{
return x < y;
}
#endif
private:
template<class type_parameter_0_0, class type_parameter_0_1>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 x, type_parameter_0_1 y)
{
return __lambda_26_19{
}.operator()<type_parameter_0_0, type_parameter_0_1>(x, y);
}
public:
// inline /*constexpr */ __lambda_26_19(const __lambda_26_19 &) noexcept = default;
// /*constexpr */ __lambda_26_19() = default;
};
CountCalls<__lambda_26_19> sc = CountCalls<__lambda_26_19>{
__lambda_26_19{
}};
std::vector<int, std::allocator<int> > v = std::vector<int, std::allocator<int> >{
std::initializer_list<int>{
5, 4, 3, 2, 1}, std::allocator<int>()};
std::sort(v.begin(), v.end(), std::ref(sc));
出力
sorted with 4 calls
1
ここでは、ラップがソート基準lambda
として使用されます。ここで参照を渡す必要があることに注意してください。そうでない場合は、取得されたコピーがパラメータとして使用され、カウント時にコピー内のカウンタのみが変更されます。ただし、アルゴリズム (非並列バージョン) は最後に渡されたコールバック関数を返し、コールバック関数の最終状態を取得するため、ラップされたものを に直接渡すことができます。std::sort()
sc
lambda
std::for_each()
std::vector v{
5,4,3,2,1};
auto fo = std::for_each(v.begin(), v.end(), CountCalls{
[](auto i) {
std::cout << "elem: " << i << '\n';
}});
std::cout << "output with " << fo.count() << " calls\n";
前処理コードは次のとおりです。
class __lambda_32_60
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 i) const
{
(std::operator<<(std::cout, "elem: ") << i) << '\n';
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ void operator()<int>(int i) const
{
std::operator<<(std::operator<<(std::cout, "elem: ").operator<<(i), '\n');
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 i)
{
return __lambda_32_60{
}.operator()<type_parameter_0_0>(i);
}
public:
// inline /*constexpr */ __lambda_32_60(const __lambda_32_60 &) noexcept = default;
// inline /*constexpr */ __lambda_32_60(__lambda_32_60 &&) noexcept = default;
// /*constexpr */ __lambda_32_60() = default;
};
CountCalls<__lambda_32_60> fo = std::for_each(v.begin(), v.end(), CountCalls<__lambda_32_60>{
__lambda_32_60{
}});
出力は次のようになります (実装が異なる可能性があるため、並べ替え基準の呼び出し数は異なる場合がsort()
あります)。
elem: 1
elem: 2
elem: 3
elem: 4
elem: 5
output with 5 calls
カウンタがアトミックな場合は、並列演算を使用することもできます。
std::sort(std::execution::par, v.begin(), v.end(), std::ref(sc));
クラステンプレートの部分引数の推論はありません
関数テンプレートとは異なり、クラス テンプレートではテンプレート パラメーターの一部のみを指定し、コンパイラーが残りを推定することを期待することはできないことに注意してください。<>
指定された空のテンプレート パラメータ リストを使用することもできません。例えば:
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
#include <string>
using namespace std;
template <typename T1, typename T2, typename T3 = T2>
class C {
public:
C(T1 x = {
}, T2 y = {
}, T3 z = {
}) {
}
};
// 推导所有参数
C c1(22, 44.3, "hi"); // OK:T1是int,T2是double,T3是const char*
C c2(22, 44.3); // OK:T1是int,T2和3是double
C c3("hi", "guy"); // OK:T1、T2、T3都是const char*
// 推导部分参数
C<string> c4("hi", "my"); // ERROR:只有T1显式指明 error: too few template arguments for class template 'C'
C<> c5(22, 44.3); // ERROR:T1和T2都没有指明 error: too few template arguments for class template 'C'
C<> c6(22, 44.3, 42); // ERROR:T1和T2都没有指明 error: too few template arguments for class template 'C'
// 指明所有参数
C<string, string, int> c7; // OK:T1、T2是string,T3是int
C<int, string> c8(52, "my"); // OK:T1是int,T2、T3是string
C<string, string> c9("a", "b", "c"); // OK:T1、T2、T3都是string
前処理コードは次のとおりです。
// 推导所有参数
C<int, double, const char *> c1 = C<int, double, const char *>(22, 44.299999999999997, "hi");
C<int, double, double> c2 = C<int, double, double>(22, 44.299999999999997, {
});
C<const char *, const char *, const char *> c3 = C<const char *, const char *, const char *>("hi", "guy", {
});
// 指明所有参数
C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> c7 = C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{
}, std::basic_string<char, std::char_traits<char>, std::allocator<char> >{
}, {
});
C<int, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > c8 = C<int, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(52, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("my", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >{
});
C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > c9 = C<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >("b", std::allocator<char>()), std::basic_string<char, std::char_traits<char>, std::allocator<char> >("c", std::allocator<char>()));
3 番目のテンプレート パラメータにはデフォルト値があるため、2 番目のパラメータを指定する限り、3 番目のパラメータを指定する必要はないことに注意してください。なぜ部分引数控除がサポートされないのか疑問に思っている場合は、この決定に至った例を次に示します。
std::tuple<int> t(42, 43); // 仍然ERROR
std::tuple
は可変個引数テンプレートであるため、任意の数のテンプレート パラメーターを指定できます。この例では、パラメータを 1 つだけ指定したのがエラーだったのか、意図的だったのかを判断することはできません。残念ながら、部分引数演繹をサポートしていないということは、一般的なコーディングのニーズに対応していないことを意味します。lambda
連想コンテナの順序付け基準や順序付けされていないコンテナのハッシュ関数を単純に使用することはまだできません。
std::set<Cust> coll([] (const Cust& x, const Cust& y) {
// 仍然ERROR
return x.getName() > y.getName();
});
lambda
まだタイプを指定する必要があります。例えば:
auto sortcrit = [] (const Cust& x, const Cust& y) {
return x.getName() > y.getName();
};
std::set<Cust, decltype(sortcrit)> coll(sortcrit); // OK
lambda
コンテナーの初期化では、指定された type でコンテナーを作成しようとするため、type を指定するだけでは機能しませんlambda
。ただし、デフォルトのコンストラクターはコンパイラによってのみ呼び出すことができるためC++17
、これは では許可されません。
ショートカット関数の代わりにクラステンプレートの引数推定を使用する
原則として、クラス テンプレート パラメーター推定を使用することで、いくつかの既存のショートカット関数テンプレートを削除できます。これらのショートカット関数の機能は、渡されたパラメーターに従って、対応するクラス テンプレートをインスタンス化することです。明らかな例としては、受信パラメータのタイプを指定するstd::make_pair()
必要がなくなることが挙げられます。たとえば、次のようなステートメントの後:
std::vector<int> v;
できるよ:
auto p = std::make_pair(v.begin(), v.end());
書く代わりに:
std::pair<typename std::vector<int>::iterator, typename std::vector<int>::iterator>
p(v.begin(), v.end());
現在、このシナリオは必要ありません。単にまたは とstd::make_pair()
書くことができます。ただし、これは別の観点からの良い例でもあり、便利な関数の役割がテンプレート パラメーターを推測することだけではないことを示しています。実際、渡されたパラメータを縮退します(特性を使用しているため、値によって渡されます)。これにより、文字列リテラル (char 配列) の型が次のように推定されます。std::pair p(v.begin(), v.end());
std::pair p{v.begin(), v.end()};
std::make_pair()
std::make_pair()
C++03
C++11
const char*
auto q = std::make_pair("hi", "world"); // 推导为指针的pair
q
前処理コードは次のとおりです。の型が であることがわかりますstd::pair<const char*, const char*>
。
std::pair<const char *, const char *> q = std::make_pair("hi", "world");
クラス テンプレートの引数の推定では、事態はさらに複雑になる可能性があります。次のようなstd::pair
単純なクラスの宣言を考えてみましょう。
template<typename T1, typename T2>
struct Pair1 {
T1 first;
T2 second;
Pair1(const T1& x, const T2& y) : first{
x}, second{
y} {
}
};
ここでは要素は参照によって渡され、言語規則に従って、パラメータが参照によって渡されたときにテンプレート パラメータの型は縮退しません。したがって、電話をかけるときは次のようになります。
Pair1 p1{
"hi", "world"}; // 推导为不同大小的数组的pair
T1
として推定されchar[3]
、T2
として推定されますchar[6]
。原理的には、このような導出は有効です。ただし、メンバーの宣言にとT1
を使用したため、次のように宣言されます。T2
first
second
char first[3];
char second[6];
ただし、左辺値の配列を使用して別の配列を初期化することはできません。これは、次のようなコードをコンパイルしようとするのと似ています。
const char x[3] = "hi";
const char y[6] = "world";
char first[3] {
x}; // ERROR
char second[6] {
y}; // ERROR
したがって、コードはPair1 p1{"hi", "world"};
次のようにエラーを報告します。
<source>:12:45: error: cannot initialize an array element of type 'char' with an lvalue of type 'const char[3]'
Pair1(const T1& x, const T2& y) : first{
x}, second{
y} {
^
<source>:17:11: note: in instantiation of member function 'Pair1<char[3], char[6]>::Pair1' requested here
Pair1 p1{
"hi", "world"};
^
<source>:12:56: error: cannot initialize an array element of type 'char' with an lvalue of type 'const char[6]'
Pair1(const T1& x, const T2& y) : first{
x}, second{
y} {
パラメータを宣言するときにパラメータを値で渡すと、この問題は発生しないことに注意してください。
tempalte<typename T1, typename T2>
struct Pair2 {
T1 first;
T2 second;
Pair2(T1 x, T2 y) : first{
x}, second{
y} {
}
};
次のように新しいオブジェクトを作成すると:
Pair2 p2{
"hi", "world"}; // 推导为指针的pair
T1
とT2
推定されますconst char*
。
前処理コードは次のとおりです。
template<typename T1, typename T2>
struct Pair1
{
T1 first;
T2 second;
inline Pair1(T1 x, T2 y)
: first{
x}
, second{
y}
{
}
};
/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Pair1<const char *, const char *>
{
const char * first;
const char * second;
inline Pair1(const char * x, const char * y)
: first{
x}
, second{
y}
{
}
};
#endif
/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair1(const char * x, const char * y) -> Pair1<const char *, const char *>;
#endif
int main()
{
Pair1<const char *, const char *> p1 = Pair1<const char *, const char *>{
"hi", "world"};
return 0;
}
ただし、コンストラクターは参照によって渡されるためstd::pair<>
、次の初期化は通常はコンパイルできません。
std::pair p{
"hi", "world"}; // 看似会推导出不同大小的数组的pair,但是……
std::pair<>
しかし、実は派生ガイドがあるのでコンパイルできるのですが、
導出ガイドライン
特定の推論ガイドを定義して、クラス テンプレート パラメーターの新しい推論を追加したり、コンストラクター定義の推論を変更したりできます。
たとえば、Pair3
テンプレート パラメーターが推定されるたびに、パラメーターが value によって渡された推定が動作するように定義できます。
#include <iostream>
#include <string>
#include <utility>
template <typename T1, typename T2>
struct Pair3 {
T1 first;
T2 second;
Pair3(const T1& x, const T2& y) : first{
x}, second{
y} {
}
};
//为构造函数定义的推到指引
template <typename T1, typename T2>
Pair3(T1, T2) -> Pair3<T1, T2>;
int main() {
Pair3 p1{
"hi", "world"}; }
->
の左側で、何を導出したいかを宣言します。ここで宣言するのは、T1
それぞれ値と型で渡される 2 つのオブジェクトを使用してオブジェクトT2
を作成することですPair3
。の右側で->
、導出の結果を定義します。この例では、Pair3
TypeT1
とT2
Instantiate を取り上げます。
前処理コードは次のとおりです
template<typename T1, typename T2>
struct Pair3
{
T1 first;
T2 second;
inline Pair3(const T1 & x, const T2 & y)
: first{
x}
, second{
y}
{
}
};
/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Pair3<const char *, const char *>
{
const char * first;
const char * second;
inline Pair3(const char *const & x, const char *const & y)
: first{
x}
, second{
y}
{
}
};
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair3(const char (&x)[3], const char (&y)[6]) -> Pair3<char[3], char[6]>;
#endif
template <typename T1, typename T2>
Pair3(T1, T2) -> Pair3<T1, T2>;
/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Pair3(const char *, const char *) -> Pair3<const char *, const char *>;
#endif
int main()
{
Pair3<const char *, const char *> p1 = Pair3<const char *, const char *>{
"hi", "world"};
return 0;
}
それはコンストラクターがすでに行っていることだと言えるかもしれません。ただし、コンストラクターは参照によって渡されるため、この 2 つは異なります。一般に、テンプレートだけでなく、値によって渡されるすべてのパラメータは縮退しますが、参照によって渡されるパラメータは縮退しません。縮退とは、ネイティブ配列がポインターに変換され、const
または 参照などのトップレベルの修飾子が無視されることを意味します。
導出ガイドがない場合は、次の宣言の場合:
Pair3 p3{
"hi", "world"};
引数x
の型は であるconst char(&)[3]
ためとT1
推定されchar[3]
、y
引数の型は であるconst char(&)[6]
ため とT2
推定されますchar[6]
。演繹ガイドを使用すると、テンプレートパラメータが縮退します。これは、渡された配列または文字列リテラルが、対応するポインター型に縮退することを意味します。ここで、次のように宣言します。
Pair3 p3{
"hi", "world"};
理解ディレクティブが機能するため、引数は値によって渡されます。したがって、どちらの型も に縮退しconst char*
、テンプレート引数推定の結果として使用されます。上記の宣言は、次の宣言と同等です。
Pair3<const char*, const char*> p3{
"hi", "world"};
コンストラクターは引き続き参照によって渡されることに注意してください。T1
推論ガイドはテンプレート パラメーターの推論にのみ関連しており、推論やT2
その後のコンストラクターの実際の呼び出しとは何の関係もありません。
演繹ディレクティブを使用した型縮退の強制
T
前の例で示したように、オーバーロードされた演繹ルールの非常に重要な用途は、演繹時にテンプレート パラメーターが確実に縮退するようにすることです。次のような古典的なクラス テンプレートを考えてみましょう。
template<typename T>
struct C {
C(const T&) {
}
...
};
ここで、文字列リテラルを渡すと"hello"
、渡される型は となるためconst char(&)[6]
、次のようにT
推定されますchar[6]
。
C x{
"hello"}; // T被推导为char[6]
その理由は、パラメーターが参照によって渡される場合、テンプレート パラメーターは対応するポインター型に縮退しないためです。
単純な導出を使用してガイドします。
template<typename T> C(T) -> C<T>;
この問題は次のように解決できます。
C x{
"hello"}; // T被推导为const char*
この演繹により、パラメーターを値で渡すことがガイドされるため、"hello"
型はT
に縮退しますconst char*
。このため、コンストラクターの引数として参照を渡すテンプレート クラスには、対応する推論ガイドが必要です。対応する導出ガイドは、C++
標準ライブラリpair
およびに提供されています。tuple
非テンプレート控除ガイドライン
演繹ガイドは必ずしもテンプレートである必要はなく、コンストラクターに必ずしも適用されるわけでもありません。たとえば、次の構造体に追加された演繹ディレクティブも有効です。
template<typename T>
struct S {
T val;
};
S(const char*) -> S<std::string>; // 把S<字符串字面量>映射为S<std::string>
ここでは、対応するコンストラクターを使用せずに演繹ガイドを作成します。推論ガイドを使用してパラメータを推論するT
と、構造体のテンプレート パラメータが指定されたものと同等になります。
したがって、以下の初期化コードはすべて正しく、T
テンプレート パラメーターは次のように推定されますstd::string
。
S s1{
"hello"}; // OK,等同于S<std::string> s1{"hello"};
S s2 = {
"hello"}; // OK,等同于S<std::string> s2 = {"hello"};
S s3 = S{
"hello"}; // OK,两个S都被推导为S<std::string>
渡された文字列リテラルは暗黙的に に変換できるためstd::string
、上記の初期化はすべて有効です。
集計にはリストの初期化が必要であることに注意してください。次のコードではパラメータ推定は正常に機能しますが、中かっこが欠落しているため初期化エラーが発生します。
S s4 = "hello"; // ERROR:不能不使用花括号初始化聚合体
S s5("hello"); // ERROR:不能不使用花括号初始化聚合体
控除ガイドとコンストラクター
演繹ガイドはクラス コンストラクターと競合します。クラス テンプレート パラメーターの推論では、オーバーロードの状況に基づいて、最も一致するコンストラクター/推論ガイドが選択されます。コンストラクターと推論ガイドの一致優先順位が同じ場合、推論ガイドが優先されます。
次の定義を考慮してください。
template<typename T>
struct C1 {
C1(const T&) {
}
};
C1(int)->C1<long>;
int
オーバーロード ルールに従って上位の一致であるため、1 つを渡すときに減点ガイドが使用されます。
したがって、T
は次のように導出されますlong
。
C1 x1{
42}; // T被推导为long
ただし、 1 つを渡すとchar
、コンストラクターは (型変換が必要ないため) よりよく一致します。つまり、次T
のように推定されますchar
。
C1 x3{
'x'}; // T被推导为char
オーバーロードルールでは、値渡しのパラメータと参照渡しのパラメータの一致度は同じになります。ただし、一致度が同じ場合は導出ガイドが優先されます。したがって、推論ガイドは通常、パラメーターを値で渡すように定義されます (これには型回帰の利点もあります)。
明示的な導出ガイド
導出ガイドをexplicit
宣言できます。この控除ディレクティブは、許可されない初期化または変換が発生した場合には無視されますexplicit
。例えば:
template<typename T>
struct S {
T val;
};
explicit S(const char*) -> S<std::string>;
この控除ディレクティブは、コピーが初期化されている(使用されている)場合には無視されます。=
これは、次の初期化が無効であることを意味します。
S s1 = {
"hello"}; // ERROR(推导指引被忽略,因此是无效的)
エラーは次のとおりです。
<source>:12:29: error: class template argument deduction for 'S<T>' failed: explicit deduction guide selected in copy-list-initialization
12 | int main() {
S s1 = {
"hello"}; }
| ^
<source>:10:10: note: explicit deduction guide declared here
10 | explicit S(const char*) -> S<std::string>;
| ^
右側の直接初期化または明示的な導出は引き続き機能します。
S s2{
"hello"}; // OK,等同于S<std::string> s2{"hello"};
S s3 = S{
"hello"}; // OK
S s4 = {
S{
"hello"}}; // OK
前処理コードは次のとおりです。
S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s2 = {
std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())};
S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s3 = S{
std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())};
S<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s4 = {
S{
std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>())}};
別の例を次に示します。
#include <iostream>
#include <string>
#include <utility>
template <typename T>
struct Ptr {
Ptr(T) {
std::cout << "Ptr(T)\n"; }
template <typename U>
Ptr(U) {
std::cout << "Ptr(U)\n";
}
};
template <typename T>
explicit Ptr(T) -> Ptr<T*>;
int main() {
Ptr p1{
42}; // 根据推导指引推导出Ptr<int*>
Ptr p2 = 42; // 根据构造函数推导出Ptr<int>
int i = 42;
Ptr p3{
&i}; // 根据推导指引推导出Ptr<int**>
Ptr p4 = &i; // 根据构造函数推导出Ptr<int*>
}
前処理コードは次のとおりです。
#include <iostream>
#include <string>
#include <utility>
template<typename T>
struct Ptr
{
inline Ptr(T)
{
std::operator<<(std::cout, "Ptr(T)\n");
}
template<typename U>
inline Ptr(U)
{
std::operator<<(std::cout, "Ptr(U)\n");
}
};
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int *>
{
inline Ptr(int *)
{
std::operator<<(std::cout, "Ptr(T)\n");
}
template<typename U>
inline Ptr(U);
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline Ptr<int>(int)
{
std::operator<<(std::cout, "Ptr(U)\n");
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline Ptr<int *>(int *);
#endif
};
#endif
/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int>
{
inline Ptr(int)
{
std::operator<<(std::cout, "Ptr(T)\n");
}
template<typename U>
inline Ptr(U);
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline Ptr<int>(int);
#endif
};
#endif
/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Ptr<int **>
{
inline Ptr(int **);
template<typename U>
inline Ptr(U);
/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline Ptr<int *>(int *)
{
std::operator<<(std::cout, "Ptr(U)\n");
}
#endif
};
#endif
/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int) -> Ptr<int>;
#endif
/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int *) -> Ptr<int *>;
#endif
template <typename T>
explicit Ptr(T) -> Ptr<T*>;
/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int) -> Ptr<int *>;
#endif
/* First instantiated from: insights.cpp:21 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
Ptr(int *) -> Ptr<int **>;
#endif
int main()
{
Ptr<int *> p1 = Ptr<int *>{
42};
Ptr<int> p2 = Ptr<int>(42);
int i = 42;
Ptr<int **> p3 = Ptr<int **>{
&i};
Ptr<int *> p4 = Ptr<int *>(&i);
return 0;
}
集計導出ガイド
クラス テンプレートの引数の推論は、推論ディレクティブを使用することにより、ジェネリック集計でもサポートされます。たとえば、次のような場合です。
template<typename T>
struct A {
T val;
};
推論ガイドなしでクラス テンプレートの引数推論を使用しようとすると、エラーが発生します。
A i1{
42}; // ERROR
A s1("hi"); // ERROR
A s2{
"hi"}; // ERROR
A s3 = "hi"; // ERROR
A s4 = {
"hi"}; // ERROR
パラメータのタイプを明示的に指定する必要がありますT
。
A<int> i2{
42};
A<std::string> s5 = {
"hi"};
ただし、演繹ガイドを使用すると、次のように集計を初期化できますA(const char*) -> A<std::string>; A(int) -> A<int>;
。
A i1{
42}; //ok
A s2{
"hi"}; // OK
A s4 = {
"hi"}; // OK
前処理コードは次のとおりです
A<int> i1 = {
42};
A<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s2 = {
std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hi", std::allocator<char>())};
A<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > s4 = {
std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hi", std::allocator<char>())};
(通常の集計の初期化と同様に) 中括弧を使用する必要があることに注意してください。それ以外の場合、型はT
正常に推定できますが、初期化が間違っています。
A s1("hi"); // ERROR:T是string,但聚合体不能初始化
A s3 = "hi"; // ERROR:T是string,但聚合体不能初始化
エラーは次のとおりです。
<source>: In function 'int main()':
<source>:15:14: error: no matching function for call to 'A<std::__cxx11::basic_string<char> >::A(const char [3])'
15 | A s1("hi"); // ERROR
| ^
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A()'
6 | struct A {
| ^
<source>:6:8: note: candidate expects 0 arguments, 1 provided
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A(const A<std::__cxx11::basic_string<char> >&)'
<source>:6:8: note: no known conversion for argument 1 from 'const char [3]' to 'const A<std::__cxx11::basic_string<char> >&'
<source>:6:8: note: candidate: 'A<std::__cxx11::basic_string<char> >::A(A<std::__cxx11::basic_string<char> >&&)'
<source>:6:8: note: no known conversion for argument 1 from 'const char [3]' to 'A<std::__cxx11::basic_string<char> >&&'
<source>:17:12: error: conversion from 'const char [3]' to non-scalar type 'A<std::__cxx11::basic_string<char> >' requested
17 | A s3 = "hi"; // ERROR
std::array
の導出ガイドは、集合導出ガイドのさらなる例です。
標準導出ガイドライン
C++17
この標準では、標準ライブラリに多くの導出ガイドラインが導入されています。
pair
およびtuple
導出ガイド
std::pair
推論ディレクティブは、クラス テンプレートの引数推論で引数の縮退型を推論するために必要です。
namespace std {
template<typename T1, typename T2>
struct pair {
...
constexpr pair(const T1& x, const T2& y); // 以引用传参
...
};
template<typename T1, typename T2>
pair(T1, T2) -> pair<T1, T2>; // 以值推导类型
}
したがって、次のように宣言します。
std::pair p{
"hi", "wrold"}; // 参数类型分别为const char[3]和const char[6]
に相当:
std::pair<const char*, const char*> p{
"hi", "world"};
std::tuple
同じアプローチが可変個引数クラス テンプレートにも使用されます。
namespace std {
template<typename... Types>
class tuple {
public:
constexpr tuple(const Types&...); // 以引用传参
template<typename... UTypes> constexpr tuple(UTypes&&...);
...
};
template<typename... Types>
tuple(Types...) -> tuple<Types...>; // 以值推导类型
}
したがって、次のように宣言します。
std::tuple t{
42, "hello", nullptr};
推定される型t
は ですstd::tuple<int, const char*, std::nullptr_t>
。
イテレータから派生
範囲を表す 2 つのイテレータから要素の型を推定できるようにするために、すべてのコンテナ クラスには、たとえばstd::vector<>
次のような推定ガイドがあります。
// 使std::vector<>能根据初始的迭代器推导出元素类型
namespace std {
template<typename Iterator>
vector(Iterator, Iterator) -> vector<typename iterator_traits<Iterator>::value_type>;
}
次の例は、実際の動作を示しています。
std::set<float> s;
std::vector v1(s.begin(), s.end()); // OK,推导出std::vector<float>
**ここでの初期化には括弧を使用する必要があることに注意してください。**中括弧を使用する場合:
std::vector v2{
s.begin(), s.end()}; // 注意:并不会推导出std::vector<float>
次に、これら 2 つのパラメータは初期値列の 2 つの要素とみなされます (オーバーロード ルールに従って、初期値列の優先順位が高くなります)。したがって、これは次と同等です。
std::vector<std::set<float>::iterator> v2{
s.begin(), s.end()};
vector
これは、2 つの要素で初期化することを意味します。最初の要素は最初の要素を指す反復子であり、2 番目の要素は最後の要素を指す反復子です。
一方、次のことを考慮してください。
std::vector v3{
"hi", "world"}; // OK,推导为std::vector<const char*>
std::vector v4("hi", "world"); // OOPS:运行时错误
v3
の宣言は 2 つの要素 (両方の要素は文字列リテラル) を含むベクトルを初期化し、 のv4
初期化により実行時エラーが発生し、コア ダンプが発生する可能性があります。
**問題は、文字列リテラルが有効なイテレータでもある文字ポインタに変換されることです。** したがって、同じオブジェクトを指していない2 つのイテレータを渡します。つまり、無効な間隔を指定しました。1 つを推測しましたstd::vector<const char>
が、2 つの文字列リテラルがメモリ内のどこにあるかによっては、例外が発生したりbad_alloc
、距離がないためにコア ダンプが発生したり、 の未定義の範囲内の文字が取得される可能性があります。
全体として、中括弧を使用することが要素vector
を初期化する最良の方法です。唯一の例外は、単一のものを渡すことです(この場合、コピーが優先されます)。他の意味を持つパラメータを渡す場合は、括弧を使用することをお勧めします。vector
いずれの場合も、std::vector<>
または他の STL コンテナーのような複雑なコンストラクターを持つクラス テンプレートの場合は、クラス テンプレートの引数演繹を使用せず、型を明示的に指定することを強くお勧めします。
std::array<>
派生する
についてはさらに興味深い例がありますstd::array<>
。要素の型と数を同時に推定できるようにするには:
std::array a{
42, 45, 77}; // OK,推导出std::array<int, 3>
代わりに、次の導出ディレクティブが (間接的に) 定義されます。
// 让std::array<>推导出元素的数量(元素的类型必须相同):
namespace std {
template<typename T, typename... U>
array(T, U...) -> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>;
}
この導出ガイドでは、折叠表达式
(is_same_v<T, U> && ...)
すべてのパラメータが同じタイプであることを確認します。したがって、次のコードは間違っています。
std::array a{
42, 45, 77.7}; // ERROR:元素类型不同
クラス テンプレート引数推定の初期化はコンパイル時のコンテキストでも機能することに注意してください。
constexpr std::array arr{
0, 8, 15}; // OK,推导出std::array<int, 3>
(順序なし) マップの導出
導出ガイドを適切に機能させるのは非常に困難です。複雑さのレベルは、連想コンテナ ( map
、multimap
、unordered_map
、 )の推論ガイドを定義することによって実証できます。unordered_multimap
これらのコンテナ内の要素のタイプは次のとおりですstd::pair<const keytype, valuetype>
。これconst
が必要なのは、要素の位置がキーの値に依存するためです。つまり、キーの値を変更するとコンテナが不整合な状態に陥ることになります。
C++17
標準では次のようになりますstd::map
。
namespace std {
template<typename Key, typename T, typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key, T>>>
class map {
...
};
}
最初に思いついた解決策は、次のようなコンストラクターを用意することでした。
map(initializer_list<pair<const Key, T>>, const Compare& = Compare(),
const Allocator& = Allocator());
次の導出ガイドラインが定義されています。
namespace std {
template<typename Key, typename T, typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key, T>>>
map(initializer_list<pair<const Key, T>>, Compare = Compare(), Allocator = Allocator())
-> map<Key, T, Compare, Allocator>;
}
すべての引数は値によって渡されるため、この演繹ディレクティブにより、渡されたコンパレータとアロケータが前述のように縮退することが可能になります。ただし、導出ガイドではコンストラクターとまったく同じ要素型を直接使用します。つまり、初期値列のキーの型は でなければなりませんconst
。したがって、次のコードは機能しません。
std::pair elem1{
1, 2};
std::pair elem2{
3, 4};
...
std::map m1{
elem1, elem2}; // 原来的C++17推导指引会ERROR
これは、elem1
と が とelem2
推定され、std::pair<int, int>
推定ガイドラインpair
の最初の要素がconst
型である必要があるため、正常に照合できないためです。したがって、まだ次のようなものを記述する必要があります。
std::map<int, int> m1{
elem1, elem2}; // OK
したがって、導出ガイドライン内の次のconst
項目を削除する必要があります。
namespace std {
template<typename Key, typename T, typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key, T>>>
map(initializer_list<pair<Key, T>>, Compare = Compare(), Allocator = Allocator())
-> map<Key, T, Compare, Allocator>;
}
ただし、縮退コンパレータとアロケータのサポートを継続するには、const
キー タイプ ペアのオーバーロード バージョンを定義する必要もあります。それ以外の場合、キー型のパラメーターを渡すときにconst
、コンストラクターを使用して型を推定するため、const
キー パラメーターと非キー パラメーターを渡すconst
ときにわずかに異なる導出結果が得られます。
スマート ポインターには導出ガイドラインがありません
C++
標準ライブラリには、減点ガイダンスがあるはずだと思われる場所が、実際には減算ガイダンスがない場合があることに注意してください。おそらく、共有ポインタと排他的ポインタに推論ガイドを持たせて、次のように記述する必要がないようにする必要があるでしょう。
std::shared_ptr<int> sp{
new int(7)};
代わりに、次のように直接記述します。
std::shared_ptr sp{
new int(7)}; // 不支持
対応するコンストラクターがテンプレートであるため、上記は間違っています。これは、暗黙的な演繹ガイドがないことを意味します。
namespace std {
template<typename T> class shared_ptr {
public:
...
template<typename Y> explicit shared_ptr(Y* p);
...
};
}
ここY
で と はT
異なるテンプレート パラメーターです。つまり、コンストラクターから推定することはできますがY
、推定することはできませんT
。これは、次の構文をサポートする機能です。
std::shared_ptr<Base> sp{
new Derived(...)};
導出ガイドラインを提供したい場合、対応する導出ガイドラインは次のように簡単に記述できます。
namespace std {
template<typename Y> shared_ptr(Y*) -> shared_ptr<Y>;
}
ただし、これにより、配列を割り当てるときにこの控除ガイドラインも適用される可能性があります。
std::shared_ptr sp{
new int[10]}; // OOPS:推导出shared_ptr<int>
よくC++
あることですが、C の厄介な問題に遭遇します。それは、オブジェクトへのポインターとオブジェクトの配列が同じ型を持つか、同じ型を持つように縮退するということです。
この質問は危険であると思われたため、C++
標準委員会はこの質問を支持しないことを決定しました。単一のオブジェクトの場合は、次のように呼び出す必要があります。
std::shared_ptr<int> sp1{
new int}; // OK
auto sp2 = std::make_shared<int>(); // OK
配列の場合:
std::shared_ptr<std::string> p(new std::string[10],
[] (std::string* p) {
delete[] p;
});
または、ネイティブ配列へのスマート ポインターをインスタンス化する新機能を使用して、次のようにします。
std::shared_ptr<std::string[]> p{
new std::string[10]};