Pytanie

Jaka jest różnica pomiędzy dedukcją typu dla słowa kluczowego autoszablonów (ang. templates) ?

Odpowiedź

Reguły dedukcji typu dla słowa kluczowego auto są w zasadzie takie same jak w przypadku szablonów. Istnieje jednak jeden wyjątek. W przypadku listy elementów zawartych w nawiasach klamrowych dla słowa kluczowego auto otrzymamy obiekt typu std::initializer_list (plik nagłówkowy initializer_list). Poniżej przedstawiam przykład potwierdzający, że w przypadku słowa kluczowego auto będziemy mieć do czynienia z obiektem typu std::initializer_list:

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

int main()
{
    auto x = { 1, 2, 3 };

    char const *name = typeid(x).name();

    std::cout << boost::core::demangle(name) << '\n';
}

Uruchom w edytorze

std::initializer_list<int>

Inaczej jest w przypadku dedukcji typu dla szablonu. Jako przykład posłuży nam funkcja foo, dla której chcemy wydedukować typ parametru:

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

template <typename T>
void foo(T t)
{
    char const *name = typeid(t).name();
    std::cout << boost::core::demangle(name) << '\n';
}

int main()
{
    foo({ 1, 2, 3 }); // { ... } has no type
}
<source>: In function 'int main()':
https://godbolt.org/z/o8TFN6
<source>:15:20: error: no matching function for call to 'foo(<brace-enclosed initializer list>)'

   15 |     foo({ 1, 2, 3 }); // { ... } has no type

      |                    ^

<source>:7:6: note: candidate: 'template<class T> void foo(T)'

    7 | void foo(T t)

      |      ^~~

<source>:7:6: note:   template argument deduction/substitution failed:

<source>:15:20: note:   couldn't deduce template parameter 'T'

   15 |     foo({ 1, 2, 3 }); // { ... } has no type

      |                    ^

Jak widać tym razem otrzymaliśmy błąd kompilacji, ponieważ w tym przypadku lista zawarta w nawiasach klamrowych nie ma typu. Aby rozwiązać ten problem musielibyśmy jawnie wskazać typ std::initializer_list (z parametryzowanym typem przechowywanym na tej liście):

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

template <typename T>
void foo(std::initializer_list<T> t)
{
    char const *name = typeid(t).name();
    std::cout << boost::core::demangle(name) << '\n';
}

int main()
{
    foo({ 1, 2, 3 });
}

Uruchom w edytorze

std::initializer_list<int>

Na zakończenie wróćmy jeszcze do słowa kluczowego auto. W przypadku jednego elementu (umieszczonego w nawiasach klamrowych), niezależnie od sposobu inicjalizacji, otrzymamy typ  std::initializer_list, Takie zachowanie zostało zdefiniowane, w standardach C++11C++14 i możesz je zaobserwować do konkretnych wersji kompilatorów.

GCC do wersji 4.9.3:

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

int main()https://godbolt.org/z/o8TFN6
{
    auto x = { 1 };
    auto y{ 1 };

    char const *nameX = typeid(x).name();
    std::cout << boost::core::demangle(nameX) << '\n';

    char const *nameY = typeid(y).name();
    std::cout << boost::core::demangle(nameY) << '\n';
}

Uruchom w edytorze

std::initializer_list<int>
std::initializer_list<int>

Clang do wersji 3.7.1:

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

int main()
{
    auto x = { 1 };
    auto y{ 1 };

    char const *nameX = typeid(x).name();
    std::cout << boost::core::demangle(nameX) << '\n';

    char const *nameY = typeid(y).name();
    std::cout << boost::core::demangle(nameY) << '\n';
}

Uruchom w edytorze

std::initializer_list<int>
std::initializer_list<int>

Standard C++17 wprowadził zmianę w zakresie inicjalizacji bezpośredniej – w tym przypadku typem dedukowanym jest typ elementu umieszczonego w nawiasach klamrowych:

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

int main()
{
    auto x = { 1 };
    auto y{ 1 };

    char const *nameX = typeid(x).name();
    std::cout << boost::core::demangle(nameX) << '\n';

    char const *nameY = typeid(y).name();
    std::cout << boost::core::demangle(nameY) << '\n';
}

Uruchom w edytorze

std::initializer_list<int>
int

Próba wstawienia więcej niż jednego elementu między nawiasy klamrowe (podczas inicjalizacji bezpośredniej) spowoduje wtedy błąd kompilacji (zgodnie z sugestią kompilatora należy wtedy stosować inicjalizację kopiującą):

#include <iostream>
#include <initializer_list>
#include <typeinfo>
#include <boost/core/demangle.hpp>

int main()
{
    auto x = { 1 };
    auto y{ 1, 2 }; // not std::initializer_list

    char const *nameX = typeid(x).name();
    std::cout << boost::core::demangle(nameX) << '\n';

    char const *nameY = typeid(y).name();
    std::cout << boost::core::demangle(nameY) << '\n';
}
<source>: In function 'int main()':

<source>:9:18: error: direct-list-initialization of 'auto' requires exactly one element [-fpermissive]

    9 |     auto y{ 1, 2 };

      |                  ^

<source>:9:18: note: for deduction to 'std::initializer_list', use copy-list-initialization (i.e. add '=' before the '{')

Warto jeszcze zaznaczyć, że wcześniejsza dedukcja (standard C++11C++14) do typu std::initializer_list potraktowana została jako błąd (ang. defect report). Poprawka wprowadzona do standardu C++17 oznaczałaby jednak złamanie kompatybilności z poprzednimi standardami. W efekcie możesz zaobserwować, że dla późniejszych (niż wskazanych powyżej) wersji kompilatorów GCCClang, poprawka ta została wprowadzona także do standardów C++11C++14.