C++ 3d.Комментарии

7b6e9298

Явные конструкторы


Разница между String s1='a'; // ошибка: нет явного преобразования char в String String s2(10); // правильно: строка для хранения 10 символов

может показаться очень тонкой...

Но она несомненно есть. И дело тут вот в чем.

Запись X a=b;

всегда означает создание объекта a класса X посредством копирования значения некоторого другого объекта класса X. Здесь может быть два варианта:

    Объект b уже является объектом класса X. В этом случае мы получим непосредственный вызов конструктора копирования: X a(b);

    Объект b объектом класса X не является. В этом случае должен быть создан временный объект класса X, чье значение будет затем скопировано: X a(X(b));

    Именно этот временный объект и не может быть создан в случае explicit-конструктора, что приводит к ошибке компиляции.

    Еще одна тонкость состоит в том, что в определенных условиях реализациям разрешено не создавать временные объекты:

    12.8 Копирование объектов классов [class.copy]

    Там, где временный объект копируется посредством конструктора копирования, и данный объект и его копия имеют один и тот же тип (игнорируя cv-квалификаторы), реализации разрешено считать, что и оригинал и копия ссылаются на один и тот же объект и вообще не осуществлять копирование, даже если конструктор копирования или деструктор имеют побочные эффекты. Если функция возвращает объекты классов и return выражение является именем локального объекта, тип которого (игнорируя cv-квалификаторы) совпадает с типом возврата, реализации разрешено не создавать временный объект для хранения возвращаемого значения, даже если конструктор копирования или деструктор имеют побочные эффекты. В этих случаях объект будет уничтожен позднее, чем были бы уничтожены оригинальный объект и его копия, если бы данная оптимизация не использовалась.

    Давайте не поленимся и напишем маленький класс, позволяющий отследить возникающие при этом спецэффекты. #include <stdio.h> #include <string.h>

    struct A { static const int nsize=10;

    char n[nsize];

    A(char cn) { n[0]=cn; n[1]=0;




    printf("%5s.A::A()\n", n); }

    A(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "беда");

    printf("%5s.A::A(const A& %s)\n", n, a.n); }

    ~A() { printf("%5s.A::~A()\n", n); }

    A& operator=(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n); } else strcpy(n, "беда");

    printf("%5s.A::operator=(const A& %s)\n", n, a.n); return *this; } };

    A f1(A a) { printf("A f1(A %s)\n", a.n); return a; }

    A f2() { printf("A f2()\n"); A b('b'); return b; }

    A f3() { printf("A f3()\n"); return A('c'); }

    int main() { { A a('a'); A b='b'; A c(A('c')); A d=A('d'); } printf("----------\n"); { A a('a'); A b=f1(a); printf("b это %s\n", b.n); } printf("----------\n"); { A a=f2(); printf("a это %s\n", a.n); } printf("----------\n"); { A a=f3(); printf("a это %s\n", a.n); } }

    Прежде всего, в main() разными способами создаются объекты a, b, c и d. В нормальной реализации вы получите следующий вывод: a.A::A() b.A::A() c.A::A() d.A::A() d.A::~A() c.A::~A() b.A::~A() a.A::~A()

    Там же, где разработчики компилятора схалтурили, появятся ненужные временные объекты, например: ... c.A::A() ?c.A::A(const A& c) c.A::~A() d.A::A() d.A::~A() ?c.A::~A() ...

    Т.е. A c(A('c')) превратилось в A tmp('c'), c(tmp). Далее, вызов f1() демонстрирует неявные вызовы конструкторов копирования во всей красе: a.A::A() ?a.A::A(const A& a) A f1(A ?a) ??a.A::A(const A& ?a) ?a.A::~A() b это ??a ??a.A::~A() a.A::~A()

    На основании a создается временный объект ?a, и передается f1() качестве аргумента. Далее, внутри f1() на основании ?a создается другой временный объект -- ??a, он нужен для возврата значения. И вот тут-то и происходит исключение нового временного объекта: b это ??a, т.е. локальная переменная main() b -- это та самая, созданная в f1() переменная ??a, а не ее копия (специально для сомневающихся: будь это не так, мы бы увидели b это ???a).



    Полностью согласен -- все это действительно очень запутано, но разобраться все же стоит. Для более явной демонстрации исключения временной переменной я написал f2() и f3(): A f2() b.A::A() ?b.A::A(const A& b) b.A::~A() a это ?b ?b.A::~A() ---------- A f3() c.A::A() a это c c.A::~A()

    В f3() оно происходит, а в f2() -- нет! Как говорится, все дело в волшебных пузырьках.

    Другого объяснения нет, т.к. временная переменная могла была исключена в обоих случаях (ох уж мне эти писатели компиляторов!).

    А сейчас рассмотрим более интересный случай -- перегрузку операторов. Внесем в наш класс соответствующие изменения: #include <stdio.h> #include <string.h>

    struct A { static const int nsize=10; static int tmpcount;

    int val; char n[nsize];

    A(int val_) : val(val_) // для создания временных объектов { sprintf(n, "_%d", ++tmpcount); printf("%5s.A::A(int %d)\n", n, val); }

    A(char cn, int val_) : val(val_) { n[0]=cn; n[1]=0;

    printf("%5s.A::A(char, int %d)\n", n, val); }

    A(const A& a) : val(a.val) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "беда");

    printf("%5s.A::A(const A& %s)\n", n, a.n); }

    ~A() { printf("%5s.A::~A()\n", n); }

    A& operator=(const A& a) { val=a.val;

    if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n); } else strcpy(n, "беда");

    printf("%5s.A::operator=(const A& %s)\n", n, a.n); return *this; }

    friend A operator+(const A& a1, const A& a2) { printf("operator+(const A& %s, const A& %s)\n", a1.n, a2.n); return A(a1.val+a2.val); } };

    int A::tmpcount;

    int main() { A a('a', 1), b('b', 2), c('c', 3); A d=a+b+c; printf("d это %s\n", d.n); printf("d.val=%d\n", d.val); }

    После запуска вы должны получить следующие результаты: a.A::A(char,int 1) b.A::A(char,int 2) c.A::A(char,int 3) operator+(const A& a,const A& b) _1.A::A(int 3) operator+(const A& _1,const A& c) _2.A::A(int 6) _1.A::~A() d это _2 d.val=6 _2.A::~A() c.A::~A() b.A::~A() a.A::~A()

    Все довольно наглядно, так что объяснения излишни. А для демонстрации работы оператора присваивания попробуйте A d('d',0); d=a+b+c;

    В данном случае будет задействовано на одну временную переменную больше: a.A::A(char,int 1) b.A::A(char,int 2) c.A::A(char,int 3) d.A::A(char,int 0) operator+(const A& a,const A& b) _1.A::A(int 3) operator+(const A& _1,const A& c) _2.A::A(int 6) =_2.A::operator=(const A& _2) _2.A::~A() _1.A::~A() d это =_2 d.val=6 =_2.A::~A() c.A::~A() b.A::~A() a.A::~A()


    Содержание раздела