14.11 — Konstruktory domyślne i argumenty domyślne

A konstruktorem domyślnym jest konstruktorem, który nie akceptuje żadnych argumentów. Zwykle jest to konstruktor zdefiniowany bez parametrów.

Oto przykład klasy, która ma domyślny konstruktor:

#include <iostream>

class Foo
{
public:
    Foo() // default constructor
    {
        std::cout << "Foo default constructed\n";
    }
};

int main()
{
    Foo foo{}; // No initialization values, calls Foo's default constructor

    return 0;
}

Gdy powyższy program zostanie uruchomiony, obiekt typu Foo jest tworzony. Ponieważ nie podano żadnych wartości inicjujących, konstruktor domyślny Foo() nazywa się, co wypisuje:

Foo default constructed

Inicjalizacja wartości a inicjalizacja domyślna dla typów klas

Jeśli typ klasy ma konstruktor domyślny, zarówno inicjalizacja wartości, jak i inicjalizacja domyślna wywołają konstruktor domyślny. Zatem dla takiej klasy jak Foo class w powyższym przykładzie następujące są zasadniczo równoważne:

    Foo foo{}; // value initialization, calls Foo() default constructor
    Foo foo2;  // default initialization, calls Foo() default constructor

Jednakże, jak już omawialiśmy na lekcji 13.9 — Domyślna inicjalizacja elementu członkowskiego, inicjalizacja wartości jest bezpieczniejsza dla agregatów. Ponieważ trudno jest stwierdzić, czy typ klasy jest zagregowany, czy nie, bezpieczniej jest po prostu używać inicjalizacji wartości do wszystkiego i nie martwić się tym.

Najlepsza praktyka

Preferuj inicjalizację wartości zamiast domyślnej inicjalizacji dla wszystkich typów klas.

Konstruktory z argumentami domyślnymi

Podobnie jak w przypadku wszystkich funkcji, parametry konstruktorów znajdujące się najbardziej na prawo mogą mieć argumenty domyślne.

Powiązana treść

Domyślne argumenty omówimy na lekcji 11.5 -- Domyślne argumenty.

Na przykład:

#include <iostream>

class Foo
{
private:
    int m_x { };
    int m_y { };

public:
    Foo(int x=0, int y=0) // has default arguments
        : m_x { x }
        , m_y { y }
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
    }
};

int main()
{
    Foo foo1{};     // calls Foo(int, int) constructor using default arguments
    Foo foo2{6, 7}; // calls Foo(int, int) constructor

    return 0;
}

Wypisuje:

Foo(0, 0) constructed
Foo(6, 7) constructed

Jeśli wszystkie parametry konstruktora mają argumenty domyślne, konstruktor jest konstruktorem domyślnym (ponieważ można go wywołać bez argumentów).

W następnej lekcji zobaczymy przykłady zastosowań, w których może to być przydatne (14.12 -- Delegowanie konstruktorów).

Przeciążone konstruktory

Ponieważ konstruktory są funkcjami, mogą zostać przeciążone. Oznacza to, że możemy mieć wielu konstruktorów, dzięki czemu możemy konstruować obiekty na różne sposoby:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // default constructor
    {
        std::cout << "Foo constructed\n";
    }

    Foo(int x, int y) // non-default constructor
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
    }
};

int main()
{
    Foo foo1{};     // Calls Foo() constructor
    Foo foo2{6, 7}; // Calls Foo(int, int) constructor

    return 0;
}

Konsekwencją powyższego jest to, że klasa powinna mieć tylko jednego konstruktora domyślnego. Jeśli zostanie podany więcej niż jeden konstruktor domyślny, kompilator nie będzie w stanie określić, którego należy użyć:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // default constructor
    {
        std::cout << "Foo constructed\n";
    }

    Foo(int x=1, int y=2) // default constructor
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
    }
};

int main()
{
    Foo foo{}; // compile error: ambiguous constructor function call

    return 0;
}

W powyższym przykładzie tworzymy instancję foo bez argumentów, więc kompilator będzie szukać konstruktora domyślnego. Znajdzie dwa i nie będzie w stanie jednoznacznie określić, którego konstruktora należy użyć. Spowoduje to błąd kompilacji.

Niejawny konstruktor domyślny

Jeśli obiekt typu klasy niezagregowanej nie ma konstruktorów zadeklarowanych przez użytkownika, kompilator wygeneruje publiczny konstruktor domyślny (dzięki czemu klasa może być inicjowana wartościowo lub domyślnie). Konstruktor ten nazywa się an niejawnym konstruktorem domyślnym.

Rozważ następujący przykład:

#include <iostream>

class Foo
{
private:
    int m_x{};
    int m_y{};

    // Note: no constructors declared
};

int main()
{
    Foo foo{};

    return 0;
}

Ta klasa nie ma konstruktorów zadeklarowanych przez użytkownika, więc kompilator wygeneruje dla nas niejawny konstruktor domyślny. Ten konstruktor zostanie użyty do utworzenia instancji foo{}.

Niejawny konstruktor domyślny jest odpowiednikiem konstruktora, który nie ma parametrów, listy inicjatorów składowych ani instrukcji w treści konstruktora. Innymi słowy, dla powyższego Foo class, kompilator generuje to:

public:
    Foo() // implicitly generated default constructor
    {
    }

Niejawny konstruktor domyślny jest przydatny głównie wtedy, gdy mamy klasy, które nie mają elementów danych. Jeśli klasa zawiera elementy danych, prawdopodobnie będziemy chcieli umożliwić ich inicjalizację wartościami dostarczonymi przez użytkownika, a domyślny konstruktor domyślny nie będzie do tego wystarczający.

Użycie = default aby wygenerować jawnie domyślny konstruktor domyślny

W przypadkach, gdy napisalibyśmy domyślny konstruktor, który jest odpowiednikiem niejawnie wygenerowanego konstruktora domyślnego, możemy zamiast tego powiedzieć kompilatorowi, aby wygenerował dla nas konstruktor domyślny. Konstruktor ten nazywa się an jawnie domyślny konstruktor domyślnyi można go wygenerować za pomocą metody = default składnia:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() = default; // generates an explicitly defaulted default constructor

    Foo(int x, int y)
        : m_x { x }, m_y { y }
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
    }
};

int main()
{
    Foo foo{}; // calls Foo() default constructor

    return 0;
}

W powyższym przykładzie, ponieważ mamy konstruktor zadeklarowany przez użytkownika (Foo(int, int)), domyślny konstruktor domyślny zwykle nie zostałby wygenerowany. Ponieważ jednak powiedzieliśmy kompilatorowi, aby wygenerował taki konstruktor, tak się stanie. Konstruktor ten zostanie następnie użyty w naszej instancji foo{}.

Najlepsza praktyka

Preferuj jawnie domyślny konstruktor domyślny (= default) zamiast domyślnego konstruktora z pustą treścią.

Jawnie domyślny konstruktor domyślny vs pusty konstruktor zdefiniowany przez użytkownika

Istnieją co najmniej dwa przypadki, w których jawnie domyślny konstruktor domyślny zachowuje się inaczej niż pusty konstruktor zdefiniowany przez użytkownika konstruktor.

  1. Jeśli wartość inicjuje klasę, jeśli klasa ma domyślny konstruktor zdefiniowany przez użytkownika, obiekt zostanie domyślnie zainicjowany. Jeśli jednak klasa ma domyślny konstruktor, który nie został dostarczony przez użytkownika (tj. domyślny konstruktor, który jest albo zdefiniowany niejawnie, albo zdefiniowany za pomocą = default), obiekt zostanie zainicjowany zerem przed inicjalizacją domyślną.
#include <iostream>

class User
{
private:
    int m_a; // note: no default initialization value
    int m_b {};

public:
    User() {} // user-defined empty constructor

    int a() const { return m_a; }
    int b() const { return m_b; }
};

class Default
{
private:
    int m_a; // note: no default initialization value
    int m_b {};

public:
    Default() = default; // explicitly defaulted default constructor

    int a() const { return m_a; }
    int b() const { return m_b; }
};

class Implicit
{
private:
    int m_a; // note: no default initialization value
    int m_b {};

public:
    // implicit default constructor

    int a() const { return m_a; }
    int b() const { return m_b; }
};

int main()
{
    User user{}; // default initialized
    std::cout << user.a() << ' ' << user.b() << '\n';

    Default def{}; // zero initialized, then default initialized
    std::cout << def.a() << ' ' << def.b() << '\n';

    Implicit imp{}; // zero initialized, then default initialized
    std::cout << imp.a() << ' ' << imp.b() << '\n';

    return 0;
}

Na komputerze autora wypisuje:

782510864 0
0 0
0 0

Zauważ to user.a nie został zainicjowany zerem przed inicjalizacją domyślną i dlatego pozostał niezainicjowany.

W praktyce nie powinno to mieć znaczenia, ponieważ powinieneś zapewnić domyślne inicjatory składowe dla wszyscy członkowie danych!

Wskazówka

W przypadku klasy, która nie ma dostarczonego przez użytkownika domyślnego konstruktora, inicjalizacja wartości spowoduje najpierw inicjalizację klasy zerem, podczas gdy inicjalizacja domyślna nie. Biorąc to pod uwagę, inicjalizacja domyślna może być bardziej wydajna niż inicjalizacja wartości (kosztem mniejszego bezpieczeństwa). Jeśli chcesz maksymalnie wykorzystać wydajność w sekcji kodu, która inicjuje wiele obiektów, które nie mają dostarczonych przez użytkownika domyślnych konstruktorów, warto rozważyć zmianę tych obiektów na domyślnie inicjalizowane. Alternatywnie możesz spróbować zmienić klasę, aby mieć domyślny konstruktor z pustą treścią. Pozwala to uniknąć przypadku inicjalizacji zerowej podczas korzystania z inicjalizacji wartości, ale może utrudniać inne optymalizacje.

  1. Przed C++20 klasa ze zdefiniowanym przez użytkownika domyślnym konstruktorem (nawet jeśli ma pustą treść) sprawia, że ​​klasa nie jest zagregowana, podczas gdy jawnie domyślny konstruktor domyślny tego nie robi. Zakładając, że w przeciwnym razie klasa byłaby agregacją, ta pierwsza spowodowałaby, że klasa użyłaby inicjalizacji listy zamiast inicjalizacji agregacji. W C++ 20 i nowszych rozwiązano tę niespójność, tak że obie czynią klasę niezagregowaną.

Twórz konstruktor domyślny tylko wtedy, gdy ma to sens

Konstruktor domyślny pozwala nam tworzyć obiekty typu klasy niezagregowanej, bez wartości inicjujących podanych przez użytkownika. Zatem klasa powinna udostępniać domyślny konstruktor tylko wtedy, gdy ma sens tworzenie obiektów danego typu klasy przy użyciu wszystkich wartości domyślnych.

Na przykład:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    Fraction() = default;
    Fraction(int numerator, int denominator)
        : m_numerator{ numerator }
        , m_denominator{ denominator }
    {
    }

    void print() const
    {
        std::cout << "Fraction(" << m_numerator << ", " << m_denominator << ")\n";
    }
};

int main()
{
    Fraction f1 {3, 5};
    f1.print();

    Fraction f2 {}; // will get Fraction 0/1
    f2.print();

    return 0;
}

W przypadku klasy reprezentującej ułamek sensowne jest zezwolenie użytkownikowi na tworzenie obiektów Fraction bez inicjatorów (w takim przypadku użytkownik otrzyma ułamek 0/1).

Rozważ teraz tę klasę:

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
private:
    std::string m_name{ };
    int m_id{ };

public:
    Employee(std::string_view name, int id)
        : m_name{ name }
        , m_id{ id }
    {
    }

    void print() const
    {
        std::cout << "Employee(" << m_name << ", " << m_id << ")\n";
    }
};

int main()
{
    Employee e1 { "Joe", 1 };
    e1.print();

    Employee e2 {}; // compile error: no matching constructor
    e2.print();

    return 0;
}

W przypadku klasy reprezentującej pracownika nie ma sensu pozwalać tworzenie pracowników bez imienia. Zatem taka klasa nie powinna mieć domyślnego konstruktora, aby w przypadku próby wykonania tego przez użytkownika klasy wystąpił błąd kompilacji.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
64 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze