Pytanie

Jaki jest output poniższego programu ? Odpowiedź uzasadnij.

#include <iostream>
#include <string>

class Person
{
public:
   Person(std::string const & name)
   {
      std::cout << "Person(std::string const & name)\n";         
   }
   
   Person(Person const & other)
   {
      std::cout << "Person(Person const & other)\n";         
   }
   
   template <typename T>
   Person(T && t)
   {
      std::cout << "Person(T && t)\n"; 
   }
};

int main()
{
   Person p1("Rafal");
    
   std::string name("Adam");
   Person p2(name);
    
   Person p3(p1);
}

Uruchom w edytorze

Odpowiedź

Person(T && t)                                                                                                                                                                              
Person(T && t)                                                                                                                                                                              
Person(T && t)

Rezultat może okazać się dla wielu zaskakujący, ale po kolei. Zacznijmy od określenia jakie konstruktoryzdefiniowaneklasie Person. Pierwszy z nich przyjmuje parametr będący referencją do stałego obiektu typu std::string (name). Kolejny to konstruktor kopiujący, a więc jako parametr przyjmuje referencję do stałego obiektu typu Person (other). Ostatni z nich to szablon konstruktora, który przyjmuje jako parametr referencjęforwardującą” (ang. forward reference) zwaną inaczej uniwersalną referencję (ang. universal reference), a więc może pobierać zarówno l-wartości jak i r-wartości.

Wróćmy teraz do naszego outputu. Jak widać każdy z obiektów klasy Person (p1, p2, p3) jest tworzony wyłącznie za pośrednictwem konstruktora, którego parametrem jest uniwersalna referencja. Przyjrzyjmy się każdemu z trzech przypadkach z osobna, jednocześnie uzasadniając odpowiedź na postawione pytanie.

Przypadek 1

Person p1("Rafal")

Tutaj do konstruktora przekazujemy argument będący łańcuchem tekstowym (c-string), którego rzeczywistym typem jest tablica składająca się z sześciu znaków (dodatkowo uwzględniając znak terminujący ‚\0‚), a więc const char[6]. Po degeneracji (ang. decay) do typu wskaźnikowego, czyli const char *, parametr taki mógłby być przekazany do konstruktora przyjmującego jako parametr referencję do stałego obiektu typu std::string (plik nagłówkowy string), bowiem jeden z konstruktorów klasy std::string przyjmuje parametr typu const char *. Oznacza to, że wymagana byłaby konwersjatypu const char * do typu std::string. Wymagana konwersja oznacza jednak, iż nie mamy tutaj idealnego dopasowania. Konsekwencją tego jest wybór konstruktoraparametrem będącym uniwersalną referencją, bowiem konstruktor taki wygenerowany na podstawie szablonu będzie cechował się dokładnym dopasowaniem.

Przypadek 2

std::string name("Adam");
Person p2(name);

Tym razem przekazany argument wydaje się być idealnie dopasowany do pierwszego konstruktora, ponieważ jego typ to std::string. Nadal nie jest to jednak idealne dopasowanie, ponieważ przekazany argument name nie jest stały (konstruktor przyjmuje jako parametr referencję do stałego obiektu typu std::string). W efekcie ponownie wybrany zostaje  konstruktorparametrem będącym uniwersalną referencją.

Przypadek 3

Person p3(p1);

Intencją autora tego kodu źródłowego było utworzenie obiektu p3 poprzez kopiowanie obiektu p1. Do tego celu powinien zostać wybrany konstruktor kopiujący, przyjmujący jako parametr referencję do stałego obiektu typu Person. Podobnie jak w poprzednim przypadku, ze względu na niestałość przekazanego argumentu (typu Person), także i tu wybrany zostaje konstruktorparametrem będącym uniwersalną referencją