C++/Qt - Konstruktory & kopiujący operator przypisania

0

Konstruktor domyślny tworzy obiekt według jakichś kryteriów - a w zasadzie nadaje obiektowi jakiś stan początkowy

i teraz nasuwają się pytania

  1. Czy konstruktor kopiujący powinien kopiować obiekt kropka w kropkę dokładnie tak samo jak tworzy go konstruktor domyślny ?

  2. Patrząc na konstruktor kopiujący i konstruktor domyślny - to czy kopiujący operator przypisania powinien kopiować obiekt kropka w kropkę dokładnie tak, jak tworzy go konstruktor domyślny ?

3

Zasada minimalnego zaskoczenia. Jak robisz kopię, to oczekujesz dokładnie identycznego obiektu niezależnego od pierwotnego. Jak to osiągniesz, twoja sprawa…

1

Konstruktor kopiujący i operator przypisnia powinny tworzyć logicznie zupełnie identyczne obiekty wg operatorów ==, <, itd. Konstruktor domyślny nijak się do tego nie ma.

0

@Althorion:

Zasada minimalnego zaskoczenia. Jak robisz kopię, to oczekujesz dokładnie identycznego obiektu niezależnego od pierwotnego. Jak to osiągniesz, twoja sprawa…

czyli myślę, że dobrze rozumiem, że jak tworzę obiekt konstruktorem domyślnym, to konstruktor kopiujący i kopiujący operator przypisania jest w sumie dokładną kopią konstruktora domyślnego ALE interfejs klasy PO PROSTU musi dostarczyć funkcjonalność przypisania i skopiowania obiektu konstruktorem kopiującym, ponieważ w ten sposób dostarczamy w pełni funkcjonalną klasę ?

3

Z opisu wydaje mi się, że nie rozumiesz. Załóżmy np. klasę Integer. Jej konstruktor domyślny może np. ustawiać wartość 0, albo jakiś NaN, ale kopiujący/przypisania powinien ustawiać wartość przekazaną z drugiego obiektu.

0

@kq:

Konstruktor kopiujący i operator przypisnia powinny tworzyć logicznie zupełnie identyczne obiekty wg operatorów ==, <, itd. Konstruktor domyślny nijak się do tego nie ma.

czyli wniosek nasuwa się taki, że mam zapewnić w pełni funkcjonalny interfejs tzn.

Jeżeli stworzę obiekt konstruktorem domyślnym np Klasa nazwa_obiektu() to tworzę obiekt z jakimś wewnętrznym stanem domyślnym i teraz jeżeli stworzę drugi obiekt tym samym konstruktorem ALE POTEM ustawię mu jakiś inny stan za pomocą dostępnych metod w klasie to już mam inny stan niż początkowy który nadałem za pomocą konstruktora domyślnego, to od konstruktora kopiującego oczekuję aby ten obiekt był dokładną kopią ze stanem jaki ustawiłem ?

No to teraz jak wygląda kwestia z operatorem przypisania ?

0

@Azarien:

dobrze napisał, chodzi o to że operator przypisania powinien utworzyć nowy obiekt identyczny według operatora ==, czyli po a = b; powinno zachodzić a == b.

no ja się domyślam, że operator = i konstruktor kopiujący

Klasa obiekt1(); //konstruktor domyślny

Klasa obiekt2(obiekt1); //konstruktor kopiujący

Klasa obiekt3();

obiekt3 = obiekt2 //operator przypisania

Klasa obiekt4, obiekt5;

obiekt5.setNapis("jakiś tekst"); //zmieniam stan wewnętrzny obiektu5 na inny niż domyślny

obiekt4 = obiekt5; //operator przypisania zmianę stanu wewnętrznego obiektu5 powinien móc uwzględnić
//i to samo dotyczy konstruktora kopiującego

powinny utworzyć mi DOKŁADNIE taki sam obiekt jak tworzę konstruktorem domyślnym, mając na uwadze również to, że gdy ustawię inny stan wewnętrzny tego obiektu za pomocą metod dostępnych w klasie, to te operatory również powinny potrafić zrobić dokładnie taką samą kopię - niezależnie od tego czy utworzyłem obiekt z domyślnym ustawieniem czy ten stan jeszcze zmieniłem metodami

0

^ tak, dobrze rozumiesz.

1

Jeżeli stworzę obiekt konstruktorem domyślnym np Klasa nazwa_obiektu() to tworzę obiekt z jakimś wewnętrznym stanem domyślnym i teraz jeżeli stworzę drugi obiekt tym samym konstruktorem ALE POTEM ustawię mu jakiś inny stan za pomocą dostępnych metod w klasie to już mam inny stan niż początkowy który nadałem za pomocą konstruktora domyślnego, to od konstruktora kopiującego oczekuję aby ten obiekt był dokładną kopią ze stanem jaki ustawiłem ?

Jeszcze raz — oczekujesz, że kopia będzie wyglądała jak oryginał, nie jak coś innego. Jak masz int x, y; x = 5; y = x, to oczekujesz, że y będzie taki sam, jak x. Nie taki, jak domyślny int (czyli zero) ani nic takiego.

No to teraz jak wygląda kwestia z operatorem przypisania ?

Oczekujesz, że zmieni stan obiektu na taki, jaki najlepiej pozwala reprezentować wartość przypisywaną. W szczególności, jak jest tej samej klasy, to dokładnie taką samą.

0

@alagner:

^ tak, dobrze rozumiesz.

no to już teraz rozumiem czemu w operatorze przypisania i w konstruktorze kopiującym jest znaczek &

czyli

Klasa &operator=(Klasa &obiekt);

Klasa(Klasa &obiekt);
0

To nie są poprawne deklaracje, brakuje const

0

@kq:

To nie są poprawne deklaracje, brakuje const

czyli powinno być

Klasa &operator=(const Klasa &obiekt);

Klasa(const Klasa &obiekt);
4

Tak, no chyba że po napisaniu a=b; oczekujesz że b zostanie zmienione.

1

Nikt tu nie powiedział wyrażnie, skopiowany obiekt na sens lub nie, zależy jaką rzeczywistość w świecie zewnętrznym reprezentuje.

Np kartoteka Jan Kowalski PESEL 12345678900 w jakiejś ewidencji nie powinna dać się kopiować.

Wątek mówi o Qt, wiele obiektów Qt też nie ma sensu kopiować

0

@zkubinski jedna rzecz. Jak używasz konstruktora domyślnego, używaj {} zamiast (). https://www.fluentcpp.com/2018/01/30/most-vexing-parse/

4
zkubinski napisał(a):

No to teraz jak wygląda kwestia z operatorem przypisania ?

Oczekuje się, że operator przypisania zrobi to samo co konstruktor kopiujący z tą tylko różnicą, że operator przypisania musi wpierw usunąć stary stan obiektu (np. zwolnić pamięć). Konstruktory zaś dostają obiekt niezaincjowany (bez starego stanu).

0

@LongInteger:

Oczekuje się, że operator przypisania zrobi to samo co konstruktor kopiujący z tą tylko różnicą, że operator przypisania musi wpierw usunąć stary stan obiektu (np. zwolnić pamięć). Konstruktory zaś dostają obiekt niezaincjowany (bez starego stanu).

Bardzo ciekawe. Czy możesz podać przykład jak operator przypisania "zwalnia" stary stan obiektu ? Szczerze mówiąc nie wiem jak to osiągnąć

@_13th_Dragon:

Dla mnie (oraz wielu innych na tym forum) tak, zaś dla @zkubinski , szczerzę wątpię? Poza tym nie koniecznie konstruktor domyślny robi to samo co pusta lista inicjalizacyjna. Jak mawiał Mark Twain: - umiar jest potrzebny we wszystkim, łącznie z umiarem.

Rozważmy najprostszy wariant, weźmy za przykład (w duuużym uproszczeniu) mój program, który próbuję zrobić z zapisem pliku JSON. Mam najprostszy typ konstruktora jaki może być

ProstaKlasa::ProstaKlasa() : ipv4Address("127.0.0.1")
{
}

czyli według ciebie nie mogę dla tego typu konstruktora użyć

ProstaKlasa obiekt{};

Natomiast dla takiego konstruktora

SkomplikowanaKlasa::SkomplikowanaKlasa() : // tu ze dwadzieścia zmiennych do inicjalizacji
{
// tu konstruktor tworzy bardzo skomplikowany obiekt
}

to dla powyższego konstruktora według ciebie mogę już użyć ?

SkomplikowanaKlasa DuzyObiekt{};

Jaka jest różnica między jednym, a drugim użyciem { } ?

PS. czytałem podlinkowany artykuł i wnioski z niego są takie

  1. Zawsze i wszędzie używaj { } aby nie narobić sobie problemu z debugowaniem, autor nie wspominał aby nie używać { } NAWET dla prostego konstruktora domyślnego.

  2. O dziwo gdzieś mi się obiło o oczy, że standard C++11 wspiera zapis konstruktorów nawiasami { } i nie widziałem potrzeby używania tego zapisu, aż do momentu przeczytania podlinkowanego artykułu. Myślę, że jest sens użycia { } dla każdego przypadku konstruktora chociażby dla czytelności - czyli odróżnienia konstruktora od funkcji

Chyba, że masz na myśli taki zapis w najprostszej wersji

SkomplikowanaKlasa DuzyObiekt(InnyMniejszyObiekt{});

Co nie zmienia faktu, że użycie { } NIE jest zabronione przy dosłownie każdym konstruktorze.

DOPISEK:
już chyba rozumiem po co używać { } w konstruktorze - rozumiem, to tak, że to zastępuje listę inicjalizacyjną konstruktora - czyli jeżeli nie została zadeklarowana lista inicjalizacyjna, to żeby nadać "stan początkowy" obiektu używa się nawiasów klamrowych { }

więc dlatego kod który dałeś (przerobiłem po swojemu) bo jak zwykle lubisz udziwiać

#include <QVector>
#include <QDebug>

using namespace std;

int main()
{
    QVector <int>v1(3,5); //tutaj nie zainicjalizuje się poprawnie
    int i;

    foreach(i, v1){
        qDebug()<< i;
    }

    qDebug()<< Qt::endl;

    QVector <int>v2{3,5}; //tutaj zainicjalizuje się poprawnie bo użyłem nawiasów klamrowych
    int j;

    foreach(j, v2){
        qDebug()<< j;
    }

    return 0;
}

tylko nie rozumiem:

  1. jak samemu napisać konstuktor aby można było użyć nawiasów { } aby możliwa była poprawna inicjalizacja parametrów konstruktora ? Poda ktoś przykład ?
  2. jak rozpoznać przypadek, że jest mi potrzebne użycie nawiasów ( ) albo { }
1

Bardzo ciekawe. Czy możesz podać przykład jak operator przypisania "zwalnia" stary stan obiektu ? Szczerze mówiąc nie wiem jak to osiągnąć

Wyobraź sobie, że ręcznie piszesz stringa. Masz potem kod w stylu MojString x = "Ala ma kota"; x = "Kot ma Alę"; Oczekujesz, że jak x dostanie nowy ciąg znaków do przechowywania, to pozbędzie się jakoś elegancko tego poprzedniego, tak żeby nie było wycieku pamięci.

Jaka jest różnica między jednym, a drugim użyciem { } ?

Żadna. Oba wywołują konstruktor bez żadnych parametrów — konstruktor domyślny. Gdyby jakieś parametry były, to znalazłyby się pomiędzy tymi klamrami.

Co nie zmienia faktu, że użycie { } NIE jest zabronione przy dosłownie każdym konstruktorze.

Ja nawet uważam, że jest zalecane, bo dzięki temu nie sposób pomylić z deklaracją funkcji, nawet jak człowiek jest zmęczony; ale to jest kwestia stylu.

jak samemu napisać konstuktor aby można było użyć nawiasów { } aby możliwa była poprawna inicjalizacja parametrów konstruktora ? Poda ktoś przykład ?

Albo napisać konstruktor przyjmujący initializer_list w sposób jawny, albo nie — wtedy kompilator spróbuje inicjalizacji agregacyjnej.

jak rozpoznać przypadek, że jest mi potrzebne użycie nawiasów ( ) albo { }

Do tworzenia funkcji potrzebne Ci są nawiasy okrągłe. Do inicjalizacji do C++17 włącznie nawiasy okrągłe potrzebne są, gdy nie chcesz wykonać konstruktora z listy inicjalizacyjnej, w C++20 można użyć desygnatora {.nazwa_parametru = wartość}. Przykład dla C++17 i tego rozróżnienia:

auto p = new std::vector<int>{1};  // dostaje jednoelementową listę inicjalizacyjną z 1 — stworzy więc jednoelementowy wektor z 1
auto q = new std::vector<int>(1);  // dostaje „czystą” jedynkę — stworzy więc jednoelementowy wektor z domyślnie konstruowanymi intami
std::cout << p->at(0) << '\n';
std::cout << q->at(0) << '\n';
3
zkubinski napisał(a):

@Azarien:

dobrze napisał, chodzi o to że operator przypisania powinien utworzyć nowy obiekt identyczny według operatora ==, czyli po a = b; powinno zachodzić a == b.

no ja się domyślam, że operator = i konstruktor kopiujący
powinny utworzyć mi DOKŁADNIE taki sam obiekt jak tworzę konstruktorem domyślnym, mając na uwadze również to, że gdy ustawię inny stan wewnętrzny tego obiektu za pomocą metod dostępnych w klasie, to te operatory również powinny potrafić zrobić dokładnie taką samą kopię

Z tą dokładnością to bym nie przesadzał, jakiś stan wewnętrzny może się różnić jeśli ma to uzasadnienie – nie musi być to kopia bit-per-bit (wtedy operator nie miałby sensu…). Ale obiekty powinny być przynajmniej na tyle „identyczne” na ile pozwala sprawdzienie operatorami takimi jak ==, <=, >= itp.

0

@Althorion:

auto p = new std::vector<int>{1}; // dostaje jednoelementową listę inicjalizacyjną z 1 — stworzy więc jednoelementowy wektor z 1
auto q = new std::vector<int>(1); // dostaje „czystą” jedynkę — stworzy więc jednoelementowy wektor z domyślnie konstruowanymi intami
std::cout << p->at(0) << '\n';
std::cout << q->at(0) << '\n';

@enedil:

Różnicę między {} a () może sobie zostaw na potem?

Aby można było użyć zapisu z nawiasami { }

auto p = new std::vector<int>{1};

to należy zrobić szablon klasy jak w artykule ?

2

@zkubinski: po toaletowej dyskusji z @_13th_Dragon słowo wyjaśniena:
zwróciłem Ci uwagę bo zapis:

struct St {}; //definicja typu
St st(); //potem gdzieś w funkcji

zostanie zinterpretowany zostanie przez kompilator jako deklaracja funkcji st, zwracającej obiekt typu St przyjmującej zero parametrów.
Żeby tego uniknąć możesz np. napisać
St st{}. Albo auto st = St(). Czy też jak Smok sugeruje, najkrócej St st. Możliwości jest trochę, generalnie zmierzam do tego żeby na to uważać. To nie jest tak, że zapis z wykorzystaniem () nie ma zastosowania, ma. Niemniej - z mojego doświadczenia przynajmniej - najłatwiej się na "most vexing parse" naciąć właśnie przy domyślnych konstruktorach.

5

Przykład różnicy między operatorem przypisania, a konstruktorem kopiującym:

#include <cstddef>

template<typename T>
class shared_ptr {
    struct shared_state {
        T* ptr;
        std::size_t count;
    };
    shared_state* state;

public:
    shared_ptr(const T& data) {
        state = new shared_state;
        state->count = 1;
        state->ptr = new T(data);
    }
    shared_ptr(const shared_ptr& other) {
        if (this == &other) return;
        state = other.state;
        state->count++;
    }
    shared_ptr& operator=(const shared_ptr& other) {
        if (this == &other) return *this;
        clear();
        state = other.state;
        state->count++;
        return *this;
    }
    ~shared_ptr() {
        clear();
    }
private:
    void clear() {
        if (state->count == 1) {
            delete state->ptr;
            delete state;
        } else {
            state->count--;
        }
        state = nullptr;
    }
};

mam nadzieję, że nic nie zbugowałem.

2
alagner napisał(a):

@zkubinski: po toaletowej dyskusji z @_13th_Dragon słowo wyjaśniena:

zwróciłem Ci uwagę bo zapis:

struct St {}; //definicja typu
St st(); //potem gdzieś w funkcji

zostanie zinterpretowany zostanie przez kompilator jako deklaracja funkcji st, zwracającej obiekt typu St przyjmującej zero parametrów.
Żeby tego uniknąć możesz np. napisać
St st{}. Albo auto st = St(). Czy też jak Smok sugeruje, najkrócej St st. Możliwości jest trochę, generalnie zmierzam do tego żeby na to uważać. To nie jest tak, że zapis z wykorzystaniem () nie ma zastosowania, ma. Niemniej - z mojego doświadczenia przynajmniej - najłatwiej się na "most vexing parse" naciąć właśnie przy domyślnych konstruktorach.

Live demo

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