Specyfikator typu auto oznacza, że kompilator ma wywnioskować typ na
podstawie typu wyrażenia inicjalizującego. Kompilator podstawia
wywniowskowany typ za specyfikator auto. Tego specyfikatora można
użyć w definicji typu:
zmiennej,
parametru funkcji,
parametru wyrażenia lambda,
wartości zwracanej przez funkcję.
Specyfikator typu auto pozwala na pisanie uogólnionego kodu, bo już
nie musimy podawać konkretnego typu, a możemy poprosić kompilator o
wywniowskowanie go.
Pisanie typów w starym C++ było niewygodne, pracochłonne, a przy tym
łatwo można było popełnić błędy, których kompilator czasami nie był w
stanie wychwycić. Typowy przykład: żeby iterować po kontenerze
kontenerów, musieliśmy wyliterować typ iteratora. Teraz łatwo
zadeklarować iterator definiując jego typ z użyciem specyfikatora
auto. Oto przykład:
#include <deque>
#include <iostream>
#include <vector>
int
main()
{
std::deque<std::vector<int>> d;
// We iterate using iterators with an explicitely declared type.
for(std::deque<std::vector<int>>::iterator i = d.begin();
i != d.end(); ++i)
for(std::vector<int>::iterator j = i->begin();
j != i->end(); ++j);
// We iterate using iterators, but let the compiler deduce the type.
for(auto i = d.begin(); i != d.end(); ++i)
for(auto j = i->begin(); j != i->end(); ++j);
}
Podobnie dla kontenera typu T możemy użyć funkcji size, która
zwraca wartość typu T::size_type, ale łatwiej jest nam użyć auto:
#include <iostream>
#include <vector>
using namespace std;
int
main()
{
vector<int> v = {1, 2, 3};
auto size = v.size();
// We can iterate backward with an index.
for(auto i = v.size(); i--;)
cout << v[i] << endl;
// We can iterate forward with an index. We can ask the comiler to
// deduce the type from 0, but we cannot be sure it will be the same
// as vector<int>::size_type.
for(auto i = 0; i < v.size(); ++i)
cout << v[i] << endl;
// This is correct, but without type deduction.
for(vector<int>::size_type i = 0; i < v.size(); ++i)
cout << v[i] << endl;
// We ask the compiler to take the type of an expression without
// deduction. This is correct, and the most general, but somehow I
// don't like it.
for(decltype(v.size()) i = 0; i < v.size(); ++i)
cout << v[i] << endl;
}
Czasami nie jesteśmy w stanie podać typu, bo go nie znamy, jak w przypadku domknięcia, czyli funktora typu anonimowego, który jest wynikiem wyrażenia lambda.
int
main()
{
auto c = []{};
}
Na razie sprawa wydaje się prosta, bo w definicji typu użyliśmy tylko
auto, ale definicja typu może zawierać dodatkowo także kwalifikatory
i deklaratory.
Wnioskowanie typu auto jest takie same, jak wnioskowanie
typowych argumentów szablonu.
Inicjalizacja zmiennej wygląda tak:
type name = expression;
Typ type zmiennej name może zawierać kwalifikatory (const,
volatile). Dodatkowo type może zawierać deklarator & typu
referencyjnego i deklarator * typu wskaźnikowego. Interesuje nas
sytuacja, kiedy typ zmiennej zawiera specyfikator auto. Na
przykład:
const auto &t = 1;
Kompilator traktuje taką inicjalizację zmiennej jak inicjalizację parametru funkcji w szablonie funkcj, gdzie:
auto jest traktowane jak nazwa typowego parametru szablonu,
wyrażenie inicjalizujące jest traktowane jak argument funkcji.
Zadaniem kompilatora jest wywnioskowanie argumentu takiego urojonego
szablonu (urojonego, bo nie jest zapisany w kodzie, a jedynie go sobie
wyobrażamy) i podstawienie go w miejsce auto.
Poniższe przykłady nie powinny być trudne do zrozumienia, bo znamy już zasady wnioskowania. Żeby sprawdzić, że poprawnie myślimy (wnioskujemy), możemy w przykładach wykorzystać poniższą sztuczkę. Kompilacja zakończy się błędem, w którym będzie podany wywnioskowany typ.
template <typename T>
class ER;
int
main()
{
// auto = int
auto x = 1;
// Uncomment the line to see the type of x.
// ER<decltype(x)> er;
}
A tu wersja wariadyczna:
template <typename... T>
class ER;
template <typename... T>
void foo(T... t)
{
ER<T...> er;
}
void goo(auto... t)
{
ER<decltype(t)...> er;
}
int
main()
{
// Uncomment the lines to see the types reported.
// foo(1, .1);
// goo(1, .1);
}
Możemy zadeklarować referencję do danej typu, który kompilator ma sam wywnioskować. Daną może być inna zmienna, funkcja czy tablica. Oto przykład:
void
foo()
{
}
int
main()
{
const volatile int x = 1;
// A reference to a variable.
// auto = const volatile int
auto &r1 = x;
// auto = volatile int
const auto &r2 = x;
// auto = const int
volatile auto &r3 = x;
// auto = int
const volatile auto &r4 = x;
// A reference to a function.
// auto = void()
auto &f1 = foo;
// The above is equivalent to this.
using ft = void();
ft &f2 = foo;
// A reference to a C-style table.
int t[] = {1, 2, 3};
// auto = int[3]
auto &t1 = t;
// The above is equivalent to this.
using t3i = int[3];
t3i &t2 = t;
}
Podobnie dla wskaźników:
void foo()
{
}
int
main()
{
const volatile int x = 1;
// A pointer to a variable.
// auto = const volatile int
auto *r1 = &x;
// auto = volatile int
const auto *r2 = &x;
// auto = const int
volatile auto *r3 = &x;
// auto = int
const volatile auto *r4 = &x;
// A pointer to a function.
// auto = void()
auto *f1 = foo;
// The above is equivalent to this.
using ft = void();
ft *f2 = foo;
// A pointer to a C-style table.
int t[] = {1, 2, 3};
// auto = int[3]
auto *t1 = &t;
// The above is equivalent to this.
using t3i = int[3];
t3i *t2 = &t;
}
Używając typu zwykłego (niereferencyjnego i niewskaźnikowego), możemy inicjalizować zmienną bez podawania jej typu. W ten sposób możemy upewnić się, że zmienna jest zainicjalizowana. Pamiętajmy, że to jedynie sztuczka, a nie jakaś mądrość programowania w C++.
Jeżeli wyrażenie inicjalizujące jest typu wskaźnikowego, to wywnioskowany typ będzie wskaźnikowy. W tym przypadku, wyrażenia inicjalizujące takie jak nazwa funkcji, nazwa tablicy, czy literał łańcuchowy rozpadną się (na wskaźnik).
Dla zmiennej zwykłego typu nigdy nie będzie wywnioskowany typ referencyjny, bo wyrażenie inicjalizujące nigdy nie jest typu referencyjnego.
double foo()
{
return .0;
}
int *goo()
{
return static_cast<int *>(0);
}
int &loo()
{
static int l;
return l;
}
int
main()
{
// auto = int
auto w = 1;
// auto = int
const auto x = 2;
// auto = int
auto y = w;
// auto = int
auto z = x;
// auto = double
auto a = foo();
// auto = int *
auto b = goo();
// auto = double (*)()
auto fp = foo;
int t[] = {1, 2, 3};
// auto = int *
auto tp = t;
// auto = const char *
auto hw = "Hello World!";
// auto = int
auto l = loo();
}
decltypeW miejsce specyfikator typu decltype kompilator podstawia typ
zmiennej albo wyrażenia, które są argumentem spefycikatora.
Podstawiony typ może być dowolny, także referencyjny. Ale chwileczkę,
czy przypadkiem nie było powiedziane, że wyrażenia nigdy nie są typu
referencyjnego? Czy nie powinno być tak samo z decltype? A więc, w
przypadku decltype deklarator & najwyższego rzędu nie jest
usuwany: tak powiada standard.
#include <cassert>
#include <utility>
int &foo()
{
static int i = 1;
return i;
}
int main()
{
double a = 0.1;
// Variable b is of the double type.
decltype(a) b = 1;
int i, j = 1;
int &x = i;
// Variable y has the same type as variable x: an lvalue reference.
decltype(x) y = j;
y = 2;
assert(j == 2);
// Expression foo() is of the reference type to an integer, and so
// is variable z.
decltype(foo()) z = foo();
z = 2;
assert(foo() == 2);
int &&r = 1;
// Variable s is of the rvalue reference type to an integer.
decltype(r) s = std::move(r);
s = 2;
assert(r == 2);
}
Jeżeli chcemy, żeby specyfikator decltype dostarczył typ wyrażenia
inicjalizacyjnego, to używamy decltype(auto). To nie to samo, co
auto, który stosuje zasady wnioskowania typowego argumentu szablonu.
Oto przykłady:
#include <cassert>
int &
singleton()
{
static int i = 0;
return i;
}
int main()
{
// auto = int
auto i = singleton();
i = 1;
assert(singleton() == 0);
// decltype(auto) = int &
decltype(auto) r = singleton();
r = 1;
assert(singleton() == 1);
}
auto w pętli for dla zakresuSpecyfikator auto możemy użyć w pętli for dla zakresu, czyli w
definicji typu deklarowanej zmiennej, tej dostępnej w ciele pętli.
Chociaż użycie specyfikatora auto jest wygodne, to nie musimy z
niego korzystać i możemy podać typ jawnie. Ale trzeba uważać, żeby
nie popełnić błędu.
Przykład niżej pokazuje, jak łatwo można popełnić błąd, który jest
trudny do wychwycenia. To jest błąd, który sam popełniłem, a którego
nie rozumiałem przez długi czas. W przykładzie błędnie napisany jest
typ zmiennej: const pair<int, string> &. Wydaje się, że jest
dobrze, bo chcemy iterować używając referencji stałej do elementów
kontenera, a wiemy, że elementem kontenera jest para typów klucza i
wartości. Program się kompiluje, ale nie działa prawidłowo. Gdzie
jest błąd?
Błąd jest w typie pierwszego elementu pary: klucze w kontenerze są
typu stałego, a my zażądaliśmy typu niestałego. Zatem typ zmiennej
pętli powinien być const pair<const int, string> &. Ten drobny błąd
powoduje, że kompilator tworzy tymczasową parę elementów typu int
oraz string i inicjalizuje ją przez kopiowanie wartości z pary w
kontenerze. W ten sposób mamy, co chcieliśmy, czyli referencję stałą
do pary żądanego typu.
Problem w tym, że ta tymczasowa para wkrótce wyparuje, bo jest alokowana na stosie jako dana lokalna ciała pętli. Problem, bo w wektorze zapisujemy referencję do ciągu znaków w parze, a ta referencja po zakończeniu iteracji odnosi się do nieistniejącego obiektu. Wypisując zawartość wektora widzimy ten sam ciąg znaków, bo tymczasowe pary były tworzone na stosie w tym samym miejscu, a my widzimy ostatnią wartość.
Ponieważ w kontenerach nie możemy przechowywać referencji (const
string &), to użyliśmy std::reference_wrapper<const string>.
Moglibyśmy użyć po prostu wskaźnika, ale std::reference_wrapper
możemy używać podobnie jak referencję (chodzi o składnię i semantykę).
#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <vector>
using namespace std;
int
main()
{
map<int, string> m = { {1, "Alice"}, {2, "Bob"} };
vector<std::reference_wrapper<const string>> names;
for(const pair<int, string> &e: m)
names.push_back(std::ref(e.second));
for(const auto &e: names)
cout << e.get() << endl;
}
Możemy zdefiniować typ wyniku funkcji z użyciem specyfikatora auto.
W definicji tego typu możemy użyć kwalifikatorów (const, volatile)
oraz deklaratorów (&, *).
W miejsce specyfikatora auto kompilator podstawia typ wywnioskowany
na podstawie wyrażenia instrukcji powrotu, które jest wyrażeniem
inicjalizującym zwracanej wartości. Sytuacja jest analogiczna do
inicjalizacji parametru funkcji w szablonie funkcji, z tą różnicą, że
zwracana wartość nie ma nazwy.
Oto kilka przykładów:
// auto = int[3]
auto &f1()
{
static int t[] = {1, 2, 3};
return t;
}
void foo()
{
};
auto *f2()
{
return foo;
}
// auto = const int *
auto f3()
{
static const int i = 0;
return &i;
}
int main()
{
// Function f1 returns an lvalue reference to a table of 3 integers,
// and so we are able to initialize the reference below.
int (&f1r1)[3] = f1();
// The following is equivalent to the above.
using f1t = int[3];
f1t &f1r2 = f1();
// We get a pointer to a function.
void (*f)() = f2();
// Here we just get a pointer to a const int.
const int *r3 = f3();
}
Piszemy callable f, które wywołuje jakiś inny callable g. Nie
znamy typu wyniku zwracanego przez g, ale chcemy, żeby f zwracała
tą samą daną, jaką otrzymała od g. Jest to problem doskonałego
zwracania wyniku funkcji, w którym chodzi o:
zapobiegnięcie kopiowaniu albo przenoszeniu danej,
zachowanie kategorii wartości wyrażenia wywołania g, czyli
wyrażenie wywołania f ma mieć tę samą kategorię.
Rozwiązanie: typ wyniku f ma być taki sam jak typ wyniku g.
Konstruktor (kopiujący, przenoszący) dla przekazywanego wyniku nie
będzie wywołany, bo jeżeli g zwraca wynik przez:
referencję, to f też zwraca przez referencję tego samego typu
(l-referencję, referencję stałą czy r-referencję), a taka
inicjalizacja referencji nie wywoła konstruktora,
wartość, to f też zwraca przez wartość tego samego typu, a
wtedy konstruktor zostanie pominięty.
W poprawnej implementacji, callable f powinno mieć zadeklarowany typ
wracanej wartości jako decltype(auto), a wyrażenie wywołania
callable g powinno być wyrażeniem instrukcji powrotu callable f.
Specyfikator decltype(auto) gwarantuje nam identyczny typ.
Specyfikator auto wnioskowałby typ, a tego nie chcemy.
Oto przykład:
#include <iostream>
int &g1()
{
static int i = 1;
return i;
}
int g2()
{
return 1;
}
template <typename G>
decltype(auto) f(G g)
{
return g();
}
int main()
{
f(g1) = 2;
std::cout << g1() << std::endl;
// Does not compile, because we can't assign to an rvalue.
// f(g2) = 2;
}
autoW wyrażeniu lambda, specyfikatora auto możemy użyć w definicji typu
parametru lub wyniku.
W wyrażeniu lambda możemy zdefiniować parametry operatora wywołania z
użyciem auto. Wtedy kompilator definiuje szablon składowej funkcji
operatora wywołania, gdzie auto służy jako typowy parametr szablonu.
Wywołanie domknięcia konkretyzuje szablon funkcji składowej. Oto
przykład:
#include <iostream>
using namespace std;
int main()
{
auto c = [x = 0](auto i) mutable
{
cout << __PRETTY_FUNCTION__ << ", "
<< "x = " << ++x << ", "
<< "i = " << i << endl;
};
c(1);
c(.1);
c("Hello!");
}
Domyślnym typem wyniku operator wywołania funkcji domknięcia jest
auto. Z użyciem -> możemy jednak zdefiniować typ zwracanego
wyniku jako decltype(auto), żeby zadbać o doskonałe zwracanie
wyniku.
#include <iostream>
int &
g()
{
static int a = 1;
std::cout << a << std::endl;
return a;
}
int main()
{
// auto f = []() {return g();};
auto f = []() -> decltype(auto) {return g();};
f() = 10;
g();
}
Szablon funkcji możemy skrócić o nagłówek szablonu, jeżeli typ
parametru funkcji zdefiniujemy z użyciem specyfikatora auto:
#include <iostream>
auto my_abs(auto t)
{
if (t < 0)
return -t;
return t;
}
int main()
{
std::cout << my_abs(-1) << std::endl;
std::cout << my_abs(-1ll) << std::endl;
std::cout << my_abs(-.1) << std::endl;
}
Jeżeli chcemy, żeby dwa parametry były tego samego typu, to
specyfikator auto o to nie zadba, bo każde auto tworzy nowy
parametr szablonu:
#include <iostream>
auto my_max(auto a, auto b)
{
if (a < b)
return b;
return a;
}
int main()
{
std::cout << my_max(1, 2) << std::endl;
// std::cout << my_max(1, .1) << std::endl;
}
Szablon funkcji z paczkami parametrów możemy skrócić przez użycie
specyfikatora typu auto, jak w przykładzie niżej. Jednak w tym
przykładzie, parametr T musi być zdefiniowany, bo kompilator nie
jest w stanie wywnioskować jego argumentu (nie ma parametru funkcji,
który używa T), no i dodatkowo posługujemy się nim w ciele funkcji.
#include <iostream>
#include <string>
#include <vector>
template <typename T>
auto
factory(auto... p)
{
return T{p...};
}
int
main()
{
std::cout << factory<std::string>("Hello!") << std::endl;
auto p = factory<std::vector<int>>(1, 2, 3);
}
Specyfikator auto pozwala na wnioskowanie typu.
W pętli for warto używać specyfikatora auto.
W wyrażeniu lambda, auto jest poręczne.
Gdzie możemy użyć specyfikatora auto?
Jaka jest różnica między specyfikatorami auto i decltype(auto)?
Na czym polega doskonałe zwracanie wartości?