Передача копии класса в функцию
От: andy1618 Россия  
Дата: 18.03.13 10:40
Оценка:
Встретился интересный учебный пример:
#include <iostream>

class A
{
public:
    A() { 
        std::cout << "+A"; 
    }
    ~A() { 
        std::cout << "-A"; 
    }
};

class B : public A
{
public:
    B() { 
        std::cout << "+B"; 
    }
    ~B() { 
        std::cout << "-B"; 
    }
};

void func( A a)
{
}

void main()
{
    B b;
    func(b);
}


Выдача по нему: +A+B-A-A-B-A
При передаче параметра в функцию, по-видимому, вызывается дефолтный копирующий конструктор (в выдачу он не попадает), соответственно, третья -A объясняется вызовом парного деструктора
при выходе из функции. А вот чем объяснить четвёртую -A?
Re: Передача копии класса в функцию
От: ak239  
Дата: 18.03.13 11:10
Оценка: 2 (1)
Здравствуйте, andy1618, Вы писали:
A>Выдача по нему: +A+B-A-A-B-A
A>При передаче параметра в функцию, по-видимому, вызывается дефолтный копирующий конструктор (в выдачу он не попадает), соответственно, третья -A объясняется вызовом парного деструктора
A>при выходе из функции. А вот чем объяснить четвёртую -A?

Собрал пример с помощью gcc, вывод: +A+B-A-B-A

Вообще делать так, как написано в примере — плохо, по двум причинам:
— происходит срезка всего того, что относится к производному классу. Когда есть наследование необходимо использовать передачу по ссылке, либо по указателю на базовый класс.
— лишнее копирование. Иногда это оправдано, но передача по const ссылке значительно эффективней.

+A — создали базовый класс в main
+B — создали производный класс в main
не вывели копирующий конструктор
-A — разрушили объект созданный при вызове функции
-B — разрушили производный в main
-A — разрушили базовый в main
Re: Передача копии класса в функцию
От: ak239  
Дата: 18.03.13 11:19
Оценка: +1
Здравствуйте, andy1618, Вы писали:

A>Выдача по нему: +A+B-A-A-B-A

A>При передаче параметра в функцию, по-видимому, вызывается дефолтный копирующий конструктор (в выдачу он не попадает), соответственно, третья -A объясняется вызовом парного деструктора
A>при выходе из функции. А вот чем объяснить четвёртую -A?

К слову, если переписать код, вот так:
#include <iostream>

class A
{
public:
    A() {
        std::cout << "+A";
    }
    A(const A& a){
       std::cout << "cpy A";
    }

    ~A() {
        std::cout << "-A";
    }
};

class B : public A
{
public:
    B() {
        std::cout << "+B";
    }
    B(const B& b){
       std::cout << "cpy B";
    }

    ~B() {
        std::cout << "-B";
    }
};

void func( A a)
{
}

int main()
{
   B b;
   func(b);
   return 0;
}


Тогда выводу будет:
+A+Bcpy A-A-B-A
Копировать объект типа B, конструктором копирования класса A, который ничего не знает о B — как раз и есть пример срезки (slicing).

Если же сигнатуру функции func поменять следующим образом:
void func(const A& a)
{
}


Тогда вывод будет:
+A+B-B-A

Никаких лишних копирований и проблем со срезкой объектов.
Re: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 11:44
Оценка: 4 (1)
Здравствуйте, andy1618, Вы писали:

A>при выходе из функции. А вот чем объяснить четвёртую -A?


Ты же в студии тестируешь?
Видимо вот так:
func(A(b));

Что подтверждает asm вывод: третий -A вызывается в конце func(как и ожидается), а четвёртый уже после — в main:
; Line 32
    lea    rcx, QWORD PTR b$[rsp]
    call    ??0B@@QEAA@XZ                ; B::B
    npad    1
; Line 33
    movzx    eax, BYTE PTR $T25620[rsp]
    mov    BYTE PTR $T25625[rsp], al
    movzx    ecx, BYTE PTR $T25625[rsp]
    call    ?func@@YAXVA@@@Z            ; func
    npad    1
    lea    rcx, QWORD PTR $T25620[rsp]
    call    ??1A@@QEAA@XZ                ; A::~A
    npad    1
; Line 34
    lea    rcx, QWORD PTR b$[rsp]
    call    ??1B@@QEAA@XZ                ; B::~B

Более того, попытка поймать создание этой A добавлением
A(const A&){}

Даёт следующий код:
; Line 32
    lea    rcx, QWORD PTR b$[rsp]
    call    ??0B@@QEAA@XZ                ; B::B
    npad    1
; Line 33
    lea    rax, QWORD PTR $T25625[rsp]
    mov    QWORD PTR $T25626[rsp], rax
    lea    rdx, QWORD PTR b$[rsp]
    mov    rcx, QWORD PTR $T25626[rsp]
    call    ??0A@@QEAA@AEBV0@@Z            ; A::A
    mov    QWORD PTR tv67[rsp], rax
    mov    rcx, QWORD PTR tv67[rsp]
    call    ?func@@YAXVA@@@Z            ; func
    npad    1
; Line 34
    lea    rcx, QWORD PTR b$[rsp]
    call    ??1B@@QEAA@XZ                ; B::~B

Clang и GCC при -fno-elide-constructors не делают лишнюю копию.

P.S. обсуждение срезки абсолютно не к месту — далеко не все классы полиморфны, соответственно это не всегда проблема. Более того, иногда нужна именно копия родительского типа(допустим чтобы swap-нуться).
Re[2]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 12:04
Оценка:
P.P.S:

void func(A a){}

При:
func(b);

Происходит copy-initialization:

С++03 8.5/12 Initializers.
The initialization that occurs in argument passing, function return, throwing an exception (15.1), handling
an exception (15.3), and brace-enclosed initializer lists (8.5.1) is called copy-initialization and is equivalent
to the form

T x = a;

То есть
{
    B b;
    A a = b;
}

Должен дать такой же результат в плане вызова конструкторов, но студия выдаёт "+A+B-A-B-A".
Очевидно (учитывая правила для copy-initialization, и сигнатуру implicitly-declared copy constructor) , что в этом упрощённом случае, не должно быть лишней копии, и раз при func(b) она есть — значит в студии баг.
Re[2]: Передача копии класса в функцию
От: ak239  
Дата: 18.03.13 12:07
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Ты же в студии тестируешь?

EP>Видимо вот так:
EP>
EP>func(A(b));
EP>


Так и должно быть, копирующий конструктор вызывается неявным образом. Потому что имеет сигнатуру по умолчанию:
func(const A&);


Создаем A на стеке, как базовый B в main.
Создаем B на стеке, дополняя A в main.
Заносим на стек параметры функции f, для этого конструируем объект типа A с помощью конструктора копирования по ссылке на B.
Выходим из функции, разрушаем объекты в стеке, соответствующие функции f — деструктор A.
Выходим из main, разрушаем объекты в стеке, соответствующие функции main — деструктор B, деструктор A.

Можно более развернутое пояснение, что за код такой собирает Visual C++ ?

EP>Clang и GCC при -fno-elide-constructors не делают лишнюю копию.

Непосредственно с этим флагом не сталкивался, но он работает, только в случаях, когда в функции вызываются константные методы?
Тогда все же логичней использовать const ссылку?

EP>P.S. обсуждение срезки абсолютно не к месту — далеко не все классы полиморфны, соответственно это не всегда проблема. Более того, иногда нужна именно копия родительского типа(допустим чтобы swap-нуться).

Сходу в голову приходит только случай, когда требуется реализовать operator+ через +=, тогда по значению самое-то.
Можете привести более развернутый пример?
Re[3]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 12:31
Оценка:
Здравствуйте, ak239, Вы писали:

EP>>Ты же в студии тестируешь?

EP>>Видимо вот так:
EP>>
EP>>func(A(b));
EP>>


A>Так и должно быть, копирующий конструктор вызывается неявным образом. Потому что имеет сигнатуру по умолчанию:


Нет, ты не понял, я не менял сигнатуру func. Тут создаётся копия (direct initialized), а потом этой копией делается copy-initialization
#include <iostream>

class A
{
public:
    A() { 
        std::cout << "+A"; 
    }
    ~A() { 
        std::cout << "-A"; 
    }
};

class B : public A
{
public:
    B() { 
        std::cout << "+B"; 
    }
    ~B() { 
        std::cout << "-B"; 
    }
};

void func(A a)
{
   (void)a;
}

int main()
{
    B b;
    func(A(b));
}

В этом случае, GCC с -fno-elide-constructors выдаёт:
+A+B-A-A-B-A

Так что это именно то, что делает студия
Re[3]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 12:37
Оценка: 2 (1)
Здравствуйте, ak239, Вы писали:

EP>>Clang и GCC при -fno-elide-constructors не делают лишнюю копию.

A>Непосредственно с этим флагом не сталкивался, но он работает, только в случаях, когда в функции вызываются константные методы?
A>Тогда все же логичней использовать const ссылку?

Нет, не только константные. Это про copy-elision, которая разрешена стандартом.

EP>>P.S. обсуждение срезки абсолютно не к месту — далеко не все классы полиморфны, соответственно это не всегда проблема. Более того, иногда нужна именно копия родительского типа(допустим чтобы swap-нуться).

A>Сходу в голову приходит только случай, когда требуется реализовать operator+ через +=, тогда по значению самое-то.
A>Можете привести более развернутый пример?

Want Speed? Pass by Value.
Если ты в любом случае внутри функции делаешь копию, то отразив это непосредственно в интерфейсе — открывается возможность для copy elision.
Re[3]: Передача копии класса в функцию
От: ak239  
Дата: 18.03.13 13:04
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>P.P.S:


EP>Должен дать такой же результат в плане вызова конструкторов, но студия выдаёт "+A+B-A-B-A".

EP>Очевидно (учитывая правила для copy-initialization, и сигнатуру implicitly-declared copy constructor) , что в этом упрощённом случае, не должно быть лишней копии, и раз при func(b) она есть — значит в студии баг.

Студия выдает абсолютно адекватный вывод. +A+B /*cpy A(B&)*/ -A-B-A.

Был так заинтригован студией, что немного поэкспериментировал с отладчиком студии.
Пришел к выводу, что вывод: +A+B-A-A+A+B может быть получен только, если студия подставляет реализацию конструктора копирования по умолчанию.

Буду рад, если кто-нибудь подробно объяснит, что за оптимизацию применяет студия.
Re[4]: Передача копии класса в функцию
От: ak239  
Дата: 18.03.13 13:16
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Want Speed? Pass by Value.

EP>Если ты в любом случае внутри функции делаешь копию, то отразив это непосредственно в интерфейсе — открывается возможность для copy elision.

Спасибо за ссылку.

Появилась идея, возможно, Visual C++ вначале преобразует тип B к типу A, а потом копирует A в стек функции f. Отсюда и берется два деструктора.
Re[4]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 13:38
Оценка: 4 (1)
Здравствуйте, ak239, Вы писали:

EP>>P.P.S:

EP>>Должен дать такой же результат в плане вызова конструкторов, но студия выдаёт "+A+B-A-B-A".
EP>>Очевидно (учитывая правила для copy-initialization, и сигнатуру implicitly-declared copy constructor) , что в этом упрощённом случае, не должно быть лишней копии, и раз при func(b) она есть — значит в студии баг.
A>Студия выдает абсолютно адекватный вывод. +A+B /*cpy A(B&)*/ -A-B-A.

Студия выдаёт +A+B-A-A-B-A, которого не должно быть.


Дальнейшие эксперименты показали, что копию можно поймать добавив какой-нибудь POD в A (если добавлять что-то с явным конструктором копирования — то лишняя копия пропадает).
#include <iostream>

class A
{
public:
    __declspec(noinline) A() { 
        std::cout << "+A"; 
    }
    __declspec(noinline) ~A() { 
        std::cout << "-A"; 
    }
    char a[100];
};
class B : public A
{
public:
    __declspec(noinline) B() { 
        std::cout << "+B"; 
    }
    __declspec(noinline) ~B() { 
        std::cout << "-B"; 
    }
};
__declspec(noinline) void func(A a)
{
   (void)a;
}
int main()
{
    B b;
    func(b);
    // BTW, if use:
    //    const A &a=b;
    //    func(a);
    // then superfluous copy disapears
}

MSVC2008SP1 x64 Release, /O2 /Ob2 /FA:
Код func:
PUBLIC    ?func@@YAXVA@@@Z                ; func
; Function compile flags: /Ogtpy
;    COMDAT ?func@@YAXVA@@@Z
_TEXT    SEGMENT
a$ = 8
?func@@YAXVA@@@Z PROC                    ; func, COMDAT
; Line 27
    jmp    ??1A@@QEAA@XZ                ; A::~A
?func@@YAXVA@@@Z ENDP                    ; func
_TEXT    ENDS

Код main:
;    COMDAT main
_TEXT    SEGMENT
$T24657 = 32
$T24646 = 40
$T24647 = 144
b$ = 256
__$ArrayPad$ = 368
main    PROC                        ; COMDAT
; Line 29
$LN5:
    sub    rsp, 392                ; 00000188H
    mov    QWORD PTR $T24657[rsp], -2
    mov    rax, QWORD PTR __security_cookie
    xor    rax, rsp
    mov    QWORD PTR __$ArrayPad$[rsp], rax
; Line 30
    lea    rcx, QWORD PTR b$[rsp]
    call    ??0B@@QEAA@XZ                ; B::B
    npad    1
; Line 31
    lea    rcx, QWORD PTR $T24646[rsp]
    lea    rdx, QWORD PTR b$[rsp]
    mov    r8d, 100                ; 00000064H
    call    memcpy
    npad    1
    lea    rcx, QWORD PTR $T24647[rsp]
    lea    rdx, QWORD PTR b$[rsp]
    mov    r8d, 100                ; 00000064H
    call    memcpy
    lea    rcx, QWORD PTR $T24647[rsp]
    call    ?func@@YAXVA@@@Z            ; func
    npad    1
    lea    rcx, QWORD PTR $T24646[rsp]
    call    ??1A@@QEAA@XZ                ; A::~A
    npad    1
; Line 36
    lea    rcx, QWORD PTR b$[rsp]
    call    ??1B@@QEAA@XZ                ; B::~B
    xor    eax, eax
    mov    rcx, QWORD PTR __$ArrayPad$[rsp]
    xor    rcx, rsp
    call    __security_check_cookie
    add    rsp, 392                ; 00000188H
    ret    0
main    ENDP

Тут видно что делаются две копии.
Кстати, первая копия — T24646 — вообще не при делах. Вторая копия делается тоже с b.
Возможно это оптимизатор заменил
T24647=T24646=b;
func <- T24647;
T24646::~A();

На
T24646=b;
T24647=b;
func <- T24647;
T24646::~A();




В Debug версии, всё ещё веселее:
_TEXT    SEGMENT
b$ = 48
$T25628 = 176
$T25629 = 288
$T25634 = 400
$T25636 = 504
__$ArrayPad$ = 512
main    PROC
; Line 29
$LN5:
    push    rsi
    push    rdi
    sub    rsp, 536                ; 00000218H
    mov    rdi, rsp
    mov    rcx, 134                ; 00000086H
    mov    eax, -858993460                ; ccccccccH
    rep stosd
    mov    QWORD PTR $T25636[rsp], -2
    mov    rax, QWORD PTR __security_cookie
    xor    rax, rsp
    mov    QWORD PTR __$ArrayPad$[rsp], rax
; Line 30
    lea    rcx, QWORD PTR b$[rsp]
    call    ??0B@@QEAA@XZ                ; B::B
    npad    1
; Line 31
    lea    rdi, QWORD PTR $T25628[rsp]
    lea    rsi, QWORD PTR b$[rsp]
    mov    ecx, 100                ; 00000064H
    rep movsb
    lea    rdi, QWORD PTR $T25634[rsp]
    lea    rsi, QWORD PTR $T25628[rsp]
    mov    ecx, 100                ; 00000064H
    rep movsb
    lea    rdi, QWORD PTR $T25629[rsp]
    lea    rsi, QWORD PTR $T25634[rsp]
    mov    ecx, 100                ; 00000064H
    rep movsb
    lea    rcx, QWORD PTR $T25629[rsp]
    call    ?func@@YAXVA@@@Z            ; func
    npad    1
    lea    rcx, QWORD PTR $T25628[rsp]
    call    ??1A@@QEAA@XZ                ; A::~A
    npad    1
; Line 36
    lea    rcx, QWORD PTR b$[rsp]
    call    ??1B@@QEAA@XZ                ; B::~B
    xor    eax, eax
    mov    rdi, rax
    mov    rcx, rsp
    lea    rdx, OFFSET FLAT:main$rtcFrameData
    call    _RTC_CheckStackVars
    mov    rax, rdi
    mov    rcx, QWORD PTR __$ArrayPad$[rsp]
    xor    rcx, rsp
    call    __security_check_cookie
    add    rsp, 536                ; 00000218H
    pop    rdi
    pop    rsi
    ret    0
main    ENDP

Там три копии POD'а
T25629=T25634=T25628=b
func <- T25629
T25628::~A();

То есть — да, в relese версии оптимизатор как-то поработал
Re[5]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 18.03.13 13:43
Оценка:
Здравствуйте, ak239, Вы писали:

A>Появилась идея, возможно, Visual C++ вначале преобразует тип B к типу A, а потом копирует A в стек функции f. Отсюда и берется два деструктора.


Так я про это и говорил в первом же сообщении
Автор: Evgeny.Panasyuk
Дата: 18.03.13
:

func(A(b));

Re: Передача копии класса в функцию
От: Erop Россия  
Дата: 18.03.13 20:24
Оценка: 3 (2)
Здравствуйте, andy1618, Вы писали:

A>Выдача по нему: +A+B-A-A-B-A

A>...
A>при выходе из функции. А вот чем объяснить четвёртую -A?

Если в таких примерах завести привычку распечатывать не только +/- А/В, но и this, то есть адрес объекта, то разбираться что где и зачем случилось проще...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[6]: Передача копии класса в функцию
От: ak239  
Дата: 19.03.13 11:04
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Здравствуйте, ak239, Вы писали:


A>>Появилась идея, возможно, Visual C++ вначале преобразует тип B к типу A, а потом копирует A в стек функции f. Отсюда и берется два деструктора.


EP>Так я про это и говорил в первом же сообщении
Автор: Evgeny.Panasyuk
Дата: 18.03.13
:

EP>

EP>func(A(b));

EP>

Просто посчитал это вызовом конструктора копирования.
func((A)b);

Было бы понятней.

Но, все-таки, почему студия пытается преобразовать тип, забывая про конструктор копирования по умолчанию, а gcc использует конструктор по умолчанию.
Кто прав согласно стандарту? Как должен вести себя компилятор, пытаться сконструировать объект, используя конструктор копирования, либо пытаться преобразовать объект.
И почему поведение меняется при определении конструктора копирования.
Re[7]: Передача копии класса в функцию
От: Evgeny.Panasyuk Россия  
Дата: 19.03.13 15:33
Оценка:
Здравствуйте, ak239, Вы писали:

A>Кто прав согласно стандарту? Как должен вести себя компилятор, пытаться сконструировать объект, используя конструктор копирования, либо пытаться преобразовать объект.


http://www.rsdn.ru/forum/cpp/5103137.1
Автор: Evgeny.Panasyuk
Дата: 18.03.13


A>И почему поведение меняется при определении конструктора копирования.


Почему баг проявляется только в подмножестве case'ов? Ну это к вопрос к разработчикам компилятора — у меня нет никакого желания реверсить msvc
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.