Operator przypisania i initialaizer_list

1

Dla doświadczonych programistów to pewnie banalne, ale jakoś nie mogę tego rozgryźć.

Stworzyłem sobie taką testową klasę:

class Test
{
public:
	int Value;

	Test() : Value(0)
	{
		std::cout << "Empty constructor" << std::endl;
	}

	Test(int V) : Value(V)
	{
		std::cout << "Normal constructor" << std::endl;
	}

	Test(const Test& Copy) : Value(Copy.Value)
	{
		std::cout << "Copy constructor" << std::endl;
	}

	Test& operator=(const Test& Source)
	{
		std::cout << "Copy assignment operator" << std::endl;
		this->Value = Source.Value;
		return *this;
	}
};

Testuję w poniższy sposób:

int main()
{
	Test T1;
	T1 = {};
	T1 = { 67 };
}

I dostaję taki output:

Empty constructor
Empty constructor
Copy asignment operator
Normal constructor
Copy asignment operator

Wydaję mi się to zrozumiałe:

W pierwszej linii zostaje wywołany pusty konstruktor dla T1.
W drugiej linii zostaje wywołany pusty konstruktor dla initializer_list w {}, a potem to co utworzono, zostaje przypisane do T1 (trzecia linia)
W czwartej linii zostaje wywołany normalny konstruktor dla { 67 }, a potem utworzony obiekt zostaje przypisany do T1 (piąta linia).

Inaczej to jednak wygląda przy takim kodzie:

	Test T1 = { 20 };
	T1 = {};

Output:

Normal constructor
Empty constructor
Copy asignment operator

Z zastosowaniem wcześniejszej logiki, powinien zostać najpierw wywołany Normal constructor dla { 20 }, a potem Copy constructor dla T1. Czemu tak się nie dzieje?

1

@Crow: W drugim przypadku masz Test T1 = { 20 }; # konstruktor , {} #empty konstruktor i T1 = #Copy asignment operator. Teraz spróbuj to przeanalizować i odnieść do pierwszego przykładu.
Dodam, że nie ma tam żadnej initializer_list, poza tym pierwsza analiza jest ok.

1

@Crow: Może ten przykład ci rozjaśni

#include <iostream>

class Foo
{
public:
    Foo()
    {
        std::cout << "Konstruktor\n";
    }
    explicit Foo(int x)
    {
        std::cout << "Konstruktor przyjmujacy wartosc\n";
    }
    Foo(const Foo&)
    {
        std::cout << "Konstruktor kopiujacy\n";
    }
    Foo& operator=(const Foo&)
    {
        std::cout << "Operator przypisania\n";
        return *this;
    }
};

int main()
{
    Foo f = Foo{};
    Foo x{}; // Uwaga na most vexing parse
    std::cout << "Dupa1\n";
    f = Foo{};
    std::cout << "Dupa2\n";
    f = Foo(3);
}
Konstruktor
Konstruktor
Dupa1
Konstruktor
Operator przypisania
Dupa2
Konstruktor przyjmujacy wartosc
Operator przypisania
1

W pierwszym przypadku

    Test T1;
    T1 = { 20 };

masz 2 wyrażenia: kostruktor bez parametrów oraz przypisanie. Przed drugą linią obiekt T1 już istnieje. W drugim przypadku:

Test T2 = { 20 };

masz jedno wyrażenie, przed linią obiekt nie istnieje, jest tworzony w wyrażeniu:
https://godbolt.org/z/9hKf9eqbf

4

@Crow: wywołanie konstruktora przez znak równości to zaszłość konieczna dla kompatybilności z C. Więcej tu:
https://m.youtube.com/watch?v=7DTlWPgX6zs
EDIT: żeby nie było za prosto, już coś takiego
Test T2 = Test{ 20 }; czy auto T2 = Test{ 20 }; to jest utworzenie tymczasowego Test z wartością 20 i przesunięcie/skopiowanie go do obiektu docelowego. Przy czym od C++17 gwarantowane jest, że nastąpi copy elision i i tak wywoła się pojedynczy konstruktor:
https://godbolt.org/z/ejf5Y7zK9 (przestaw standard na 14/11 i zobacz co się stanie).

0

Dzięki za odpowiedzi!

Czyli mam rozumieć, że poniższe zapisy są tożsame?

Test T1 = { 20 };
Test T1 = Test{ 20 };

Dla większej jasności dodałem jeszcze destruktor i teraz taki kod:

Test T1 = { 20 };
std::cout << "Stop" << std::endl;
T1 = Test{ 77 };
std::cout << "Stop" << std::endl;

daje taki output:

  1. Normal constructor
  2. Stop
  3. Normal constructor
  4. Copy asignment operator
  5. Destructor
  6. Stop
  7. Destructor

Jeżeli dobrze rozumiem, Test T1 = { 20 } prowadzi do utworzenia tymczasowego obiektu Test{ 20 }, do którego referencja (adres w pamięci) zostaje przechwycona przez T1, dlatego jego destruktor nie jest wywołany od razu (jak ma to miejsce w przypadku Test{ 77 } i co widać w linii 5), tylko dopiero w linii 7, z chwilą śmierci T1 (nowego właściciela referencji)? Dobrze kombinuję?

1

U mayersa w efficient c++ rozpisywano się na ten temat o ile pamiętam. Tu podsumowanie które jest bardzo ogólne:
Braced initialization is the most widely usable initialization syntax, it prevents
narrowing conversions, and it’s immune to C++’s most vexing parse.
• During constructor overload resolution, braced initializers are matched to
std::initializer_list parameters if at all possible, even if other construc‐
tors offer seemingly better matches.
• An example of where the choice between parentheses and braces can make a
significant difference is creating a std::vector<numeric type="type"> with two
arguments.
• Choosing between parentheses and braces for object creation inside templates
can be challenging.

edit:
http://scottmeyers.blogspot.com/2015/09/thoughts-on-vagaries-of-c-initialization.html
i sprawdź jeszcze na tej jego stronie

1 użytkowników online, w tym zalogowanych: 0, gości: 1