Tworzenie obiektu za pośrednictwem konstruktora z parametrem typu std::string (przekazywanym przez wartość lub referencję do stałej), może być problematyczne jeśli jako rzeczywisty argument przekazany zostanie literał tekstowy w stylu języka C (c-string). Aby wyjaśnić to zagadnienie rozważmy prostą klasę X, której obiekt tworzony jest jak poniżej:

#include <iostream>
#include <string>

class X 
{
public:
   X(const std::string &s) 
   { 
      std::cout << "X::X(std::string) : " << s << "\n";
   } 
};

int main()
{
   X x = "Hello World !!!";       
}

W tym momencie może nasunąć się pytanie: czy przedstawiony kod skompiluje się ?

Słowo Kluczowe explicit

Na początku warto zauważyć, że przedstawiony konstruktor nie jest opatrzony słowem kluczowym explicit, a więc wydaje się, że możliwa jest niejawna konwersja typów (ang. implicit conversion), która jest przeciwieństwem rzutowania (ang. casting), czyli jawnej konwersji typów (ang. explicit conversion). Oznacza to, że możliwe jest niejawne wywołanie konstruktora konwertującego (z jednym parametrem) za pośrednictwem inicjalizacji kopiującej (ang. copy initialization):

X x = arg;

Gdyby konstruktor opatrzony był słowem kluczowym explicit, możliwe byłoby jedynie utworzenia obiektu poprzez jawne (ang. explicit) wywołanie konstruktora czyli inicjalizację bezpośrednią (ang. direct initialization):

X x(arg);

W tym przypadku w zasadzie moglibyśmy skończyć ten artykuł odpowiadając, że ze względu na słowo kluczowe explicit przedstawiony kod na pewno się nie skompiluje (bez względu na to czy arg jest typu const char * czy std::string). W tym miejscu warto zaznaczyć, iż stosowanie słowa kluczowego explicit jest na ogół dobrą praktyką, ponieważ niespodziewane niejawne konwersje są źródłem problemów i dlatego też raczej nie są zalecane.

Literał Tekstowy

Aby jednak odpowiedzieć na postawione pytanie warto przeanalizować kolejne konwersje typów, które mógłby rozważyć kompilator. Po pierwsze trzeba ustalić jakiego typu w rzeczywistości jest literał tekstowy. Z technicznego punktu widzenia jest to tablica stałych elementów o liczbie równej N + 1, gdzie N to długość c-stringa, natomiast dodatkowy element zarezerwowany jest na ”zerowy” znak terminujący łańcuch tekstowy (\0). W efekcie typem literału tekstowego ”Hello World !!!” jest: const char [16] (jego długość to 15). Jest to typ tablicowy stałych znaków (const char), który może być w sposób niejawnie degenerowany (ang. decay), czyli konwertowany do wskaźnika (ang. pointer) do stałego znaku: const char * (niejawna konwersja z tablicy do wskaźnika wywodzi się jeszcze z języka C). Taka konwersja nastąpi ponieważ jeden z konstruktorów typu std::string (parametr naszego konstruktora) ma postać:

string (const char *s);

Mamy więc pierwszą niejawną konwersję, która jednocześnie nie jest zdefiniowana przez użytkownika. Jest to konwersja wbudowana w język C++, wykonywania niejawnie i automatycznie przez kompilator.

Typ std::string

Druga niejawna konwersja wynika z powyższego konstruktora typu std::string, a więc z typu const char * na std::string, czyli na właściwy typ naszego konstruktora. Konwersja ta traktowana jest jako zdefiniowana przez użytkownika, ponieważ klasa std::string została zaimplementowana przez autorów biblioteki standardowej języka C++ (std::string nie jest typem wbudowanym). W tym momencie mamy już argument typu std::string, który za pośrednictwem konstruktora (a więc konwersji zdefiniowanej przez użytkownika), konwertowany jest do finalnego obiektu typu X.

Poniższy schemat (niejawnych konwersji) podsumowuje nasze rozważania:

const char[16] ------------> const char * ------------> std::string ------------> X                                          
               standard C++                użytkownik                użytkownik

W zasadzie wszystko wygląda w porządku, ale kompilator ma odmienne zdanie:

main.cpp:15:14: error: conversion from 'const char [16]' to non-scalar type 'X' requested
        X x = "Hello World !!!";
              ^~~~~~~~~~~~~~~~~

Konwersja Zdefiniowana Przez Użytkownika

Można więc odpowiedzieć na postawione na początku pytanie: przedstawiony kod nie skompiluje się (pomimo braku słowa kluczowego explicit). I tu dochodzimy do sedna problemu, a zarazem uzasadnienia odpowiedzi. Standard języka C++ zabrania aby dwie konwersje zdefiniowane przez użytkownika następowały jedna za drugą, a takie mają tu miejsce: z const char * na std::string oraz z std::string na X. Aby rozwiązać ten problem można przeciążyć konstruktor parametrem typu const char * (analogicznie do takiego jaki oferuje typ std::string):

#include <iostream>
#include <string>

class X 
{
public:
   X(const std::string &s)
   { 
      std::cout << "X::X(std::string) : " << s << "\n"; 
   }
   
   X(const char *s) 
   { 
      std::cout << "X::X(const char *) : " << s << "\n"; 
   }
};

int main()
{
   X x = "Hello World !!!";       
}

Uruchom w edytorze

Powyższy kod skompiluje się, za sprawą wyłącznie jednej konwersji zdefiniowanej przez użytkownika:

const char[16] ------------> const char * ------------> X                                          
               standard C++                użytkownik

Po uruchomieniu otrzymujemy spodziewany output:

X::X(const char *) : Hello World !!!

Jawne Wywołanie Konstruktora

Zatrzymajmy się jeszcze na chwilę nad przypadkiem jawnego użycia konstruktora X(const std::string&) (bez niejawnej konwersji na typ X):

#include <iostream>
#include <string>

class X 
{
public:
   X(const std::string &s)  
   { 
      std::cout << "X::X(std::string) : " << s << "\n"; 
   }
};

int main()
{
   X x("Hello World !!!");       
}

Uruchom w edytorze

Powyższy kod skompiluje się. Zobaczmy zatem jaka jest sekwencja niejawnych konwersji:

const char[16] ------------> const char * ------------> std::string                                          
               standard C++                użytkownik

Konwersja dotyczy tutaj jedynie argumentu typu std::string. Ponownie zachodzi tylko jedna konwersja zdefiniowana przez użytkownika. Przy jawnie wywołanym konstruktorze (inicjalizacja bezpośrednia) nie zachodzi natomiast druga konwersja zdefiniowana przez użytkownika (std::string na X), co w efekcie umożliwia kompilację powyższego kodu. Po uruchomieniu otrzymujemy spodziewany output:

X::X(std::string) : Hello World !!!

Niejawna konwersja dla argumentu wywoływanej funkcji jest możliwa, ponieważ tworzenie obiektu argumentu podlega zasadom inicjalizacji kopiującej.

Podsumowanie

  • Niejawna konwersja zdefiniowana przez użytkownika możliwa jest dzięki inicjalizacji kopiującej
  • Słowo kluczowe explicit zapobiega niejawnym konwersjom
  • Typem literału znakowego jest tablica stałych znaków z dodatkowym znakiem terminującym
  • Standard języka C++ definiuje niejawną konwersję między literałem tekstowym a wskaźnikiem const char *
  • Typ std::string posiada konstruktor z parametrem typu const char * (bez słowa kluczowego explicit), pozwalający na niejawną konwersję z literału tekstowego
  • Standard języka C++ nie pozwala na wykonanie sekwencji dwóch (i więcej) konwersji zdefiniowanych przez użytkownika
  • Jawne wywołanie konstruktora (z jednym argumentem) nie jest traktowane jako konwersja zdefiniowana przez użytkownika
  • Argumenty wywołanej funkcji podlegają niejawnej konwersji (inicjalizacja kopiująca)