Skocz do zawartości

C++ () vs {} jako konstruktor


Gieneq

Pomocna odpowiedź

Pisząc pytanie udało mi się na nie odpowiedzieć 😅 więc zostawię to jako ewentualny temat do dyskusji.

W C++ jest mały bałagan (albo za dużo możliwości) i podobno w CoreGuidelines jest gdzieś napisanne czego używać i gdzie. Przykładowo to trochę rzuca światła na sytuację:

vector<int> v1(10);    // vector of 10 elements with the default value 0
vector<int> v2{10};    // vector of 1 element with the value 10

vector<int> v3(1, 2);  // vector of 1 element with the value 2
vector<int> v4{1, 2};  // vector of 2 elements with the values 1 and 2

Napisane jest też, że używanie {} do zakładania zmiennych pozwala uniknąć niejawnego (implicite) rzutowania np z double na int, więc taki zapis jest preferowany:

double d{1.2};
int i{d}; //warning: narrowing conversion of 'd' from 'double' to 'int' inside { }

Dalej:

int v;         // losowa wartość (Clang) lub 0 (GCC)
std::String s; // konstruktor domyślny i wynik ""

Zgodnie z regułą zera zakładajac własny obiekt dobrze jest korzystać z domyślnych konstruktorów/destruktorów obiektów. Zakładam klasę:

struct MyPoint {
    int x;
    int y;
    friend std::ostream& operator<<(std::ostream &os, const MyPoint& p);
};

std::ostream& operator<<(std::ostream &os, const MyPoint& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os;
 }

I testuję:

MyPoint p1;   // konstruktor domyślny
MyPoint p2{}; // lista inicjalizująca lub jak nie ma to konstruktor domyślny
MyPoint p3(); // ?????

Pytanie: co robi to ostatnie? Wartość tego to 1, podobnie jak wartość int p(), itp. Ostrzeżenie to:

main.cpp: In function 'int main()':
main.cpp:25:14: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
   25 |     MyPoint p();
      |              ^~
main.cpp:25:14: note: remove parentheses to default-initialize a variable
   25 |     MyPoint p();
      |              ^~
      |              --
main.cpp:25:14: note: or replace parentheses with braces to aggregate-initialize a variable
main.cpp:28:18: warning: the address of 'MyPoint p()' will never be NULL [-Waddress]
   28 |     std::cout << p << std::endl;
      |                  ^
main.cpp:25:13: note: 'MyPoint p()' declared here
   25 |     MyPoint p();
      |             ^

Czyli w tym miejscu jest jakby deklaracja funkcji, tak samo jakbym napisał:

MyPoint p();

int main() {
	MyPoint p1 = p(); 
}

Czyli ten zapis nie pasuje do konstruktora domyślnego (bez argumentów), bo wygląda jak zwykła funkcja - tak przynajmiej twierdzi kompilator.

Ciekawe że gdy w klasie istnieje tylko konstruktor z niezerową liczbą argumentów, to domyslny konstruktor znika:

MyPoint(int v) : x{v}, y{v} {} // tu na liście inicjalizującej mogą być (), ale nie ma ograniczeń
...
int main() {
  MyPoint p4(33); // (33, 33) 
  MyPoint p5; // błąd - nie ma domyślnego
}

Więc trzeba go dodać. Wersja domyślna potraktuje domyślnie zmienne składowe (reguła zera). Tylko patrząc na to jak traktowane są typy prymityne lepiej  nie zakładać że będą mieć wartość 0 i lepiej ręcznie te 0 wpisać.

MyPoint(int v) : x{v}, y{v} {}
MyPoint() = default; // <<
// lub: MyPoint() : x{}, y{} {}
// lub: MyPoint() : x{0}, y{0} {}
// lub: MyPoint() : x(0), y(0) {}
// lub: MyPoint() : x(), y() {} //OK
// lub: MyPoint() {x = 0; y = 0;}
...
int main() {
  MyPoint p4(33); // (33, 33) 
  MyPoint p5; // OK
}

Będąc w temacie można jeszcze dodać, że użycie słowa kluczowego explicite może uchronić przed przypadkową konwersją:

explicit MyPoint(int v) : x{v}, y{v} {}
MyPoint() = default;
...
int main() {
  MyPoint p4 = 33; // Error, explicit!
  MyPoint p5{33}; // OK
}

Wracając do sytuacji z pierwszego przykładu, można używać () {} do wyboru rodzaju konstruktora:

#include <iostream>
#include <string>
#include <vector>
#include <initializer_list>


struct MyPoint {
    std::vector<int> v;
    
    MyPoint() = default;
    MyPoint(int p) : v{p} {};
    MyPoint(int p1, int p2) : v{p1, p2} {};
    MyPoint(std::initializer_list<int> && list) : v{list} {};
    friend std::ostream& operator<<(std::ostream &os, const MyPoint& p);
};

std::ostream& operator<<(std::ostream &os, const MyPoint& p) {
    for (auto& t : p.v)
        os << t << ", ";
    return os;
 }

int main() {
    MyPoint p1{122};    // konstruktor lista inicjalizująca 1 element
    MyPoint p2(1, 2);   // konstruktor 2 arrgumenty
    MyPoint p3{1, 2};   // konstruktor lista inicjalizująca 2 elementy
    MyPoint p4({1, 2}); // konstruktor lista inicjalizująca 2 elementy, jak wyżej
    MyPoint p5{1, 2, 3};   // konstruktor lista inicjalizująca 3 elementy
    //MyPoint p6(1, 2, 3);   // błąd!
    std::cout << p1 << std::endl; //122,
    std::cout << p2 << std::endl; //1, 2, 
    std::cout << p3 << std::endl; //1, 2, 
    std::cout << p4 << std::endl; //1, 2, 
    std::cout << p5 << std::endl; //1, 2, 3, 
    return 0;
}

Żeby trochę zamieszać możemy przeciążyć operator (), wtedy sytuacja może wyglądać tak:

int operator()() {
	return -123;
}
...
int main() {
  MyPoint p4;
  auto pv = p4(); //-123 int
}

i na zakończenie napisać takiego stwora:

auto v = MyPoint{}(); //lub MyPoint()()

Wnioski:

  1. Inicjowanie zmiennych {} jest preferowane bo:
    • unika się niejawnej konwersji typów,
    • automatycznie dobiera przeciężony konstruktor wychodząc od listy inicjalizującej i spadając na konstruktora z daną liczbą argumentów,
    • nie dojdzie do przypadkowego pomylenia z funkcją,
  2. gdy mamy przeciążony konstruktor z listą inicjalizującą można użyć (), {} do wyboru wariantu konstruktora,
  3. nazwa_zmiennej() to nie konstruktor domyślny taki jak NazwaKlasy()
  • Lubię! 1
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.