Kiedy używać inicjatora w nawiasach?

97

W C ++ 11 mamy nową składnię do inicjowania klas, która daje nam dużą liczbę możliwości inicjalizacji zmiennych.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

Dla każdej deklarowanej przeze mnie zmiennej muszę pomyśleć, której składni inicjalizującej powinienem użyć, a to spowalnia moją szybkość kodowania. Jestem pewien, że nie było to intencją wprowadzenia nawiasów klamrowych.

Jeśli chodzi o kod szablonu, zmiana składni może prowadzić do różnych znaczeń, więc podążanie właściwą drogą jest niezbędne.

Zastanawiam się, czy istnieje uniwersalna wskazówka, jaką składnię należy wybrać.

helami
źródło
1
Przykład niezamierzonego zachowania od {} initialisation: string (50, 'x') vs string {50, 'x'} tutaj
P i

Odpowiedzi:

65

Myślę, że następujące wskazówki mogą być dobrą wskazówką:

  • Jeśli (pojedyncza) wartość, z którą inicjujesz, ma być dokładną wartością obiektu, użyj =inicjalizacji copy ( ) (ponieważ wtedy w przypadku błędu nigdy przypadkowo nie wywołasz jawnego konstruktora, który generalnie interpretuje podaną wartość różnie). W miejscach, gdzie inicjalizacja kopiowania nie jest dostępna, sprawdź, czy inicjalizacja nawiasów klamrowych ma poprawną semantykę, a jeśli tak, użyj jej; w przeciwnym razie użyj inicjalizacji nawiasów (jeśli to również nie jest dostępne, i tak nie masz szczęścia).

  • Jeśli wartości, z którymi inicjujesz, są listą wartości, które mają być przechowywane w obiekcie (jak elementy wektora / tablicy lub rzeczywista / urojona część liczby zespolonej), użyj nawiasów klamrowych, jeśli są dostępne.

  • Jeśli wartości, z którymi inicjujesz, nie są wartościami do zapisania, ale opisują zamierzoną wartość / stan obiektu, użyj nawiasów. Przykładami są argument rozmiaru argumentu vectorlub argument nazwy pliku elementu fstream.

celtschk
źródło
4
@ user1304032: Ustawienia regionalne nie są ciągiem znaków, dlatego nie użyjesz inicjalizacji kopiowania. Ustawienie regionalne również nie zawiera łańcucha (może przechowywać ten ciąg jako szczegół implementacji, ale nie jest to jego celem), dlatego nie używałbyś inicjalizacji nawiasów klamrowych. Dlatego wytyczna mówi, aby użyć inicjalizacji nawiasów.
celtschk
2
Osobiście podobała mi się co najwyżej ta wskazówka i dobrze działa również na kodzie ogólnym. Jest kilka wyjątków ( T {}lub powodów składniowych, takich jak najbardziej irytująca analiza ), ale ogólnie uważam, że to dobra rada. Zauważ, że jest to moja subiektywna opinia, więc warto przyjrzeć się również pozostałym odpowiedziom.
helami
2
@celtschk: To nie zadziała w przypadku typów, których nie można kopiować i nie można ich przenosić; type var{};robi.
ildjarn
2
@celtschk: Nie mówię, że jest to coś, co zdarza się często, ale to mniej pisania i działa w większej liczbie kontekstów, więc jakie są wady?
ildjarn
2
Moje wytyczne z pewnością nigdy nie wymagają inicjalizacji kopiowania. ; -]
ildjarn
27

Jestem prawie pewien, że nigdy nie będzie uniwersalnej wytycznej. Moje podejście polega na używaniu zawsze aparatu kręconego, pamiętając o tym

  1. Konstruktory listy inicjalizującej mają pierwszeństwo przed innymi konstruktorami
  2. Wszystkie kontenery bibliotek standardowych i std :: basic_string mają konstruktory listy inicjalizacyjnej.
  3. Inicjalizacja nawiasów klamrowych nie pozwala na zawężenie konwersji.

Dlatego szelki okrągłe i kręcone nie są wymienne. Ale wiedza o tym, gdzie się różnią, pozwala mi w większości przypadków użyć inicjalizacji nawiasów klamrowych nad nawiasami okrągłymi (niektóre z przypadków, w których nie mogę, są obecnie błędami kompilatora).

juanchopanza
źródło
7
Nawiasy klamrowe mają tę wadę, że mogę przez pomyłkę wywołać konstruktor listy. Okrągłe nawiasy nie. Czy to nie jest powód, aby domyślnie używać nawiasów okrągłych?
helami
4
@user: int i = 0;Nie sądzę, żeby ktokolwiek int i{0}tam używał , i może to być mylące (również 0jest typu if int, więc nie byłoby zwężenia ). W pozostałych przypadkach postąpiłbym zgodnie z radą Juancho: preferuj {}, uważaj na kilka przypadków, w których nie powinieneś. Zwróć uwagę, że nie ma zbyt wielu typów, które przyjmą listy inicjatorów jako argumenty konstruktorów, możesz oczekiwać, że kontenery i typy kontenerowe (krotka ...) będą je mieć, ale większość kodu wywoła odpowiedni konstruktor.
David Rodríguez - dribeas
3
@ user1304032 to zależy, czy zależy ci na zawężeniu. Tak, więc wolę, aby kompilator powiedział mi, że int i{some floating point}to błąd, zamiast po cichu skracać.
juanchopanza
3
Odnośnie „prefer {}, uważaj na kilka przypadków, w których nie powinieneś”: Powiedzmy, że dwie klasy mają semantycznie równoważny konstruktor, ale jedna klasa ma również listę inicjalizującą. Czy dwa równoważne konstruktory powinny być nazywane inaczej?
helami
3
@helami: "Powiedzmy, że dwie klasy mają semantycznie równoważny konstruktor, ale jedna klasa ma również listę inicjalizującą. Czy dwa równoważne konstruktory powinny być nazywane inaczej?" Powiedzmy, że natknąłem się na najbardziej irytującą analizę; może się to zdarzyć na dowolnym konstruktorze dla dowolnej instancji. Dużo łatwiej jest tego uniknąć, używając po prostu {}określenia „zainicjuj”, chyba że absolutnie nie możesz .
Nicol Bolas
16

Poza kodem ogólnym (tj. Szablonami) możesz (i ja używamy) wszędzie używać nawiasów klamrowych . Zaletą jest to, że działa wszędzie, na przykład nawet w przypadku inicjalizacji w klasie:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

lub dla argumentów funkcji:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

W przypadku zmiennych nie zwracam zbytniej uwagi między stylami T t = { init };lub T t { init };, uważam, że różnica jest niewielka i w najgorszym przypadku spowoduje tylko pomocny komunikat kompilatora o niewłaściwym użyciu explicitkonstruktora.

Dla typów, które akceptują, std::initializer_listchoć oczywiście czasami std::initializer_listpotrzebne są konstruktory (na przykład klasyczny std::vector<int> twenty_answers(20, 42);). Dobrze jest wtedy nie używać aparatu ortodontycznego.


Jeśli chodzi o kod ogólny (np. W szablonach), ostatni akapit powinien wywołać pewne ostrzeżenia. Rozważ następujące:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Następnie auto p = make_unique<std::vector<T>>(20, T {});tworzy wektor o rozmiarze 2, jeśli Tjest np. int, Lub wektor o rozmiarze 20, jeśli Tjest std::string. Bardzo charakterystycznym znakiem, że dzieje się tutaj coś bardzo złego, jest to, że nie ma cechy, która może cię tutaj uratować (np. Z SFINAE): std::is_constructibledotyczy inicjalizacji bezpośredniej, podczas gdy używamy inicjalizacji nawiasami klamrowymi, która odracza do inicjalizacja wtedy i tylko wtedy, gdy żaden konstruktor nie std::initializer_listprzeszkadza. Podobnie std::is_convertiblenie pomoże.

Zbadałem, czy rzeczywiście można ręcznie rzucić cechę, która może to naprawić, ale nie jestem co do tego zbyt optymistyczny. W każdym razie nie sądzę, żeby wiele nam brakowało, myślę, że fakt, że make_unique<T>(foo, bar)wynikiem jest konstrukcja równoważna z, T(foo, bar)jest bardzo intuicyjny; zwłaszcza biorąc pod uwagę, że make_unique<T>({ foo, bar })jest zupełnie niepodobne i ma sens tylko jeśli fooi bartego samego typu.

Dlatego w przypadku kodu ogólnego używam tylko nawiasów klamrowych do inicjowania wartości (np. T t {};Lub T t = {};), co jest bardzo wygodne i wydaje mi się, że jest lepsze niż sposób C ++ 03 T t = T();. W przeciwnym razie jest to albo bezpośrednia składnia inicjalizacji (tj. T t(a0, a1, a2);), Albo czasami konstrukcja domyślna ( T t; stream >> t;jest to jedyny przypadek, w którym używam tego, co myślę).

Nie oznacza to jednak, że wszystkie nawiasy klamrowe są złe, rozważ poprzedni przykład z poprawkami:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

To nadal używa nawiasów klamrowych do konstruowania std::unique_ptr<T>, mimo że rzeczywisty typ zależy od parametru szablonu T.

Luc Danton
źródło
@interjay Niektóre z moich przykładów mogą rzeczywiście wymagać użycia typów bez znaku, np. make_unique<T>(20u, T {})aby Tbyć albo unsignedalbo std::string. Nie jestem pewien szczegółów. (Zauważ, że skomentowałem również oczekiwania dotyczące inicjalizacji bezpośredniej w porównaniu z inicjalizacją nawiasów klamrowych dotyczących np. Funkcji doskonałego przekazywania). std::string c("qux");Nie określono, aby działał jako inicjalizacja w klasie, aby uniknąć niejednoznaczności z deklaracjami funkcji składowych w gramatyce.
Luc Danton
@interjay Nie zgadzam się z Tobą w pierwszym punkcie. Zapraszam do sprawdzenia 8.5.4 Inicjalizacja listy i 13.3.1.7 Inicjalizacja przez inicjalizację listy. Jeśli chodzi o drugie, musisz przyjrzeć się bliżej temu, co napisałem (co dotyczy inicjalizacji w klasie ) i / lub gramatyce C ++ (np. Deklarator elementu członkowskiego , który odwołuje się do inicjatora nawiasów klamrowych lub równych ).
Luc Danton
Hmm, masz rację - testowałem wcześniej z GCC 4.5, co wydawało się potwierdzać to, co mówiłem, ale GCC 4.6 zgadza się z tobą. I przegapiłem fakt, że mówiłeś o inicjalizacji w klasie. Przepraszam.
interjay