Zgadzam się z konsensusem, że na ogół najlepiej jest zainicjowanie członków danych C ++ na liście inicjalizacji członków, a nie nad organem konstruktora, ale jestem sceptyczny z To wyjaśnienie

Drugi (nieefektywny) sposób budowy konstruktorów jest przydział, taki jak: Fred::Fred() { x_ = whatever; }. W tym przypadku wyrażenie wszystko powoduje oddzielny, tymczasowy obiekt, który należy utworzyć, a ten tymczasowy obiekt jest przekazywany do operatora przypisania obiektu X_. Następnie tymczasowy obiekt jest zniszczony w ;. To nieefektywne.

Czy to jest właściwie poprawne? Spodziewałbym się, że kompilator wybudowałbym domyślnym obiektem tymczasowym, który zostanie natychmiast zastępowany przez przypisanie w organizmie. Nie wiem, dlaczego spodziewałem się tego, ale przeczytałem powyższe twierdzenie, myślę, że cicho zakładałem to od lat.

Czy listy inicjalizacji członków są rzeczywiście bardziej wydajne? Jeśli tak, czy to z tego powodu?

8
spraff 16 luty 2017, 00:31

3 odpowiedzi

Najlepsza odpowiedź

Korzystanie z listy członków init,

#include <string>

struct Fred {
  Fred() : x_("hello") { }
  std::string x_;
};

int main() {
  Fred fred;
}

Clang 3.9.1 i GCC 6.3 Wygeneruj następujące elementy za pomocą -O3 -fno-exceptions (Eksplorator kompilatora):

main:                                   # @main
        xor     eax, eax
        ret

Jeśli zamiast tego wykonamy przypisanie ciała:

#include <string>

struct Fred {
  Fred() { x_ = "hello"; }
  std::string x_;
};

int main() {
  Fred fred;
}

Oba generują dużo więcej kodu, np. Clang 3.9.1 Wyjścia to:

main:                                   # @main
        push    rbx
        sub     rsp, 32
        lea     rbx, [rsp + 16]
        mov     qword ptr [rsp], rbx
        mov     qword ptr [rsp + 8], 0
        mov     byte ptr [rsp + 16], 0
        lea     rdi, [rsp]
        xor     esi, esi
        xor     edx, edx
        mov     ecx, .L.str
        mov     r8d, 5
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)
        mov     rdi, qword ptr [rsp]
        cmp     rdi, rbx
        je      .LBB0_2
        call    operator delete(void*)
.LBB0_2:
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret

.L.str:
        .asciz  "hello"

Wydaje się więc, że listy członków są naprawdę bardziej wydajne, przynajmniej w niektórych przypadkach, nawet z nowoczesnymi kompilatorami.

8
emlai 15 luty 2017, 21:58

W słowach alexandrescu i wzmacniacza; Sutter (pozycja 9) Nie pesymizuj przedwcześnie

Unikanie przedwczesnego optymalizacji nie oznacza sugerującą niezdrogi efektywność. Przez przedwczesną pesymizację, mamy na myśli pisanie takich nieodpłatnych potencjalnych nieefektywności jako:

• Definiowanie parametrów wartości przewijania, gdy odpowiednie jest odpowiednie odniesienie do wartości. (Patrz Pozycja25.)

• Korzystanie z Postfix + +, gdy wersja prefiksu jest równie dobra. (Patrz punkt 28.)

• Używanie przydziału wewnątrz konstruktorów zamiast listy inicjatora. (Patrz punkt 48.)

Za każdym razem, gdy piszesz zadania wewnątrz konstruktorów, recenzenci kodów będą w alercie: Czy coś specjalnego? Czy naprawdę chciał mieć specjalną dwustopniową inicjalizację (ponieważ istnieje ukryta domyślna konstrukcja członka wygenerowana!). Nie zaskakuj czytelników swojego kodu.

Należy pamiętać, że AlexandRescu i wzmacniacz; Sutter kontynuuje w punkcie 48, aby omówić infekcję , ale nie twierdzić, że nigdzie istnieje nieefektywność rzeczywisty w rzeczywistym zoptymalizowanym kodzie. To jest również obok punktu, chodzi o Wyrażając intencję i unikanie ryzyka nieefektywności.

9
TemplateRex 15 luty 2017, 21:51

Czy listy inicjalizacji członków są rzeczywiście bardziej wydajne? Jeśli tak, czy to z tego powodu?

Ogólnie tak. Z inicjalizacją członka przechodzącej bezpośrednio do konstruktora, w przeciwnym razie zostanie utworzony obiekt domyślny, a następnie zostanie wywołany operator przypisania. Uwaga Nie dotyczy "tymczasowych" wymienionych w podanym cytowaniu, dotyczy to samego pola.

Możesz zobaczyć to na żywo Oto

class Verbose {
public:
    Verbose() { std::cout << "Verbose::Verbose()" << std::endl; }
    Verbose( int ) { std::cout << "Verbose::Verbose(int)" << std::endl; }
    Verbose &operator=( int )  { std::cout << "Verbose::operator=(int)" << std::endl; }
};

class A {
public:
    A() : v( 0 ) {}
    A(int)  { v = 0; }
private:
    Verbose v;    
};


int main() {
    std::cout << "case 1 --------------------" << std::endl;
    A a1;
    std::cout << "case 2 --------------------" << std::endl;
    A a2( 0 );
    // your code goes here
    return 0;
}

Wynik:

case 1 --------------------
Verbose::Verbose(int)
case 2 --------------------
Verbose::Verbose()
Verbose::operator=(int)
3
Slava 15 luty 2017, 21:55