Что происходит во время инициализации класса? - PullRequest
1 голос
/ 27 августа 2011

Вот код, который меня смущает:

#include <iostream>
using namespace std;

class B {
public:
    B() {
        cout << "constructor\n";
    }
    B(const B& rhs) {
        cout << "copy ctor\n";
    }
    B & operator=(const B & rhs) {
        cout << "assignment\n";
    }
    ~B() {
        cout << "destructed\n";
    }
    B(int i) : data(i) {
        cout << "constructed by parameter " << data << endl;
    }

private:
    int data;
};

B play(B b)
{
    return b;
}

int main(int argc, char *argv[])
{
#if 1
    B t1;
    t1 =  play(5);
#endif

#if 0
    B t1 = play(5);
#endif

    return 0;
}

Среда - g ++ 4.6.0 на Fedora 15. Первый вывод фрагмента кода выглядит следующим образом:

constructor
constructed by parameter 5
copy ctor
assignment
destructed
destructed
destructed

ИВторой фрагмент кода выдает:

constructed by parameter 5
copy ctor
destructed
destructed

Почему три деструктора вызываются в первом примере, а во втором - только два?

Ответы [ 5 ]

2 голосов
/ 27 августа 2011

Первый случай:

B t1;
t1 =  play(5);
  1. Создает объект t1, вызывая конструктор по умолчанию B.
  2. Чтобы вызвать play(), с помощью B(int i) создается временный объект B.5 передается при создании объекта B и вызывается play().
  3. return b; внутри play() вызывает copy constructor для возврата копии объекта,
  4. t1 = вызывает оператор Assignemnt , чтобы присвоить возвращенную копию объекта t1.
  5. Первый деструктор, уничтожает временный объект, созданный в #3.
  6. Второй деструктор уничтожает возвращаемый временный объект в #2.
  7. Третий деструктор уничтожает объект t1.

Второй случай:

B t1 = play(5);  
  1. Временный объект классаB создается путем вызова параметризованного конструктора B, который принимает int в качестве параметра.
  2. Этот временный объект используется для вызова конструктора Copy класса B.
  3. Первый деструктор уничтожает временный объект, созданный в #1.
  4. Второй деструктор уничтожает объект t1.

Один вызов деструктора во втором случае меньше, потому что во втором случае компилятор использует Возвращаемое значение Оптимизация и исключает вызов для создания дополнительного временного объектавозвращаясь с play().Вместо этого Base объект создается в месте, где временный был бы назначен.

1 голос
/ 27 августа 2011

Сначала рассмотрим подвыражение play(5). Это выражение одинаково в обоих случаях.

В выражении вызова функции каждый параметр инициализируется копией своего аргумента (ISO / IEC 14882: 2003 5.2.2 / 4). В этом случае это включает преобразование 5 в B с использованием неявного конструктора, принимающего int для создания временного B, а затем с помощью конструктора копирования для инициализации параметра b. Однако в реализации разрешено исключать временные объекты путем прямой инициализации b с использованием конструктора преобразования из int в соответствии с правилами, указанными в 12.8.

Тип play(5) - это B и - как функция, возвращающая не-ссылку - это rvalue .

Оператор return неявно преобразует возвращаемое выражение в тип возвращаемого значения (6.6.3), а затем копирует-инициализирует (8.5 / 12) возвращаемый объект с преобразованным выражением.

В этом случае возвращаемое выражение уже имеет правильный тип, поэтому преобразование не требуется, но инициализация копии по-прежнему требуется.


Помимо оптимизации возвращаемого значения

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

Хотя в стандарте оно не названо таковым, NRVO обычно относится к первой ситуации, описанной в 12.8 / 15.

Эта конкретная оптимизация невозможна в play, поскольку b не является объектом, локальным для тела функции, это имя параметра, который уже был создан к моменту ввода функции.

(Безымянная) оптимизация возвращаемого значения (RVO) еще меньше согласуется с тем, на что она ссылается, но обычно используется для ссылки на ситуацию, когда возвращаемое выражение не является именованным объектом, а выражением, где преобразование в возвращаемый тип и copy-initialization возвращаемого объекта можно объединить так, чтобы возвращаемый объект инициализировался прямо из результата преобразования, исключающего один временный объект.

RVO не применяется в play, потому что b уже имеет тип B, поэтому copy-initialization эквивалентен direct-initialization и не является временным объект необходим.


В обоих случаях play(5) требует построения B с использованием B(int) для параметра и инициализации копирования B для возвращаемого объекта. Он также может использовать вторую копию при инициализации параметра, но многие компиляторы исключают эту копию, даже если оптимизация явно не запрашивается. Оба (или все) из этих объектов являются временными.

В выражении выражения t1 = play(5); будет вызван оператор присваивания для копирования значения возвращаемого значения play в t1, и два временных значения (параметр и возвращаемое значение play) будут уничтожены , Естественно, t1 должно быть построено до этого утверждения, и его деструктор будет вызван в конце срока его жизни.

В операторе объявления B t1 = play(5); логически t1 инициализируется с возвращаемым значением play, и в качестве выражения выражения t1 = play(5); будет использовано точно такое же количество временных. Однако это вторая из ситуаций, описанных в 12.8 / 15, когда реализация позволяет исключить временное значение, используемое для возвращаемого значения play, и вместо этого разрешить возвращаемому объекту псевдоним t1. Функция play работает точно так же, но поскольку возвращаемый объект является просто псевдонимом t1, ее оператор return эффективно инициализирует t1, и не существует отдельного временного объекта для возвращаемого значения, которое необходимо уничтожить. .

0 голосов
/ 27 августа 2011

В первом примере вы вызываете три конструктора:

  • Конструктор B() при объявлении B t1;, который также является определением, если B() общедоступен,Другими словами, компилятор попытается инициализировать любые объявленные объекты в некотором базовом допустимом состоянии и обрабатывает B() как метод для преобразования блока памяти размером B в указанное базовое допустимое состояние, чтобы методы вызывались на t1 не сломает программу.

  • Конструктор B(int), используемый как неявное преобразование;play() принимает B, но ему присваивается int, но B(int) считается методом преобразования int в B.

  • Конструктор копирования B(const B& rhs), которыйскопирует значение B, возвращаемое play(), во временное значение, чтобы у него была достаточно длинная область действия для использования в операторе присваивания.

Каждый изуказанные выше конструкторы должны совпадать с деструктором при выходе из области.

Однако во втором примере вы явно инициализируете значение t1 результатом play(), поэтому компилятор ненужно тратить циклы, предоставляя базовое состояние t1, прежде чем он присваивает копию результата play() новой переменной.Таким образом, вы только звоните

  • B(int), чтобы получить полезный аргумент для play(B)

  • B(const B& rhs), чтобы t1 былоинициализируется с (независимо от того, что ваш конструктор копирования решит) с правильной копией результатов play().

В этом случае вы не увидите третьего конструктора, потому что компилятор "eliding""возвращаемое значение play() в t1;то есть он знал, что t1 не существовал в действительном состоянии до возврата play(), поэтому он просто записывает возвращаемое значение непосредственно в память, отведенную для t1.

0 голосов
/ 27 августа 2011

Обратитесь к тому, что Als опубликовал для воспроизведения по сценарию первого сценария.

Я думаю (EDIT: неправильно; см. Ниже) разница со вторым случаем заключается в том, что компилятор был достаточно умен, чтобы использовать NRVO (именованная оптимизация возвращаемого значения) и исключить среднюю копию: вместо создания временной копии при возврате (из play), компилятор использовал фактическое «b» внутри функции play в качестве значения для конструктора копирования t1.

У Дейва Абрахамса есть статья о копировании, а вот Википедия по оптимизации возвращаемого значения .

РЕДАКТИРОВАТЬ: На самом деле, Алс также добавил игру ко второму сценарию. :)

Дополнительные правки: На самом деле, я ошибся выше. NRVO не используется ни в том, ни в другом случае, поскольку стандарт запрещает передачу копий непосредственно из аргументов функции (b в режиме воспроизведения) в местоположение возвращаемого значения функции (по крайней мере, без вставки) в соответствии с принятым для этого ответом вопрос .

Даже если бы NRVO было разрешено, мы можем сказать, что оно не использовалось, по крайней мере, в первом случае: если бы это было так, то в первом случае вообще не было бы конструктора копирования. Конструктор копирования в первом случае происходит из скрытой копии из именованного значения b (в функции воспроизведения) в скрытое местоположение возвращаемого значения для воспроизведения. В первом случае нет явной конструкции копирования, так что это единственное место, где оно может возникнуть.

На самом деле происходит следующее: NRVO не возникает ни в одном случае, и при возврате создается скрытая копия ... но во втором случае компилятор смог создать скрытую копию возврата непосредственно в местоположении t1 , Таким образом, копия из b в возвращаемое значение не была исключена, а копия из возвращаемого значения в t1 была. Однако компилятору было труднее выполнить эту оптимизацию для первого случая, когда t1 уже был построен (читай: он этого не делал;)). Если t1 уже создан по адресу, несовместимому с местоположением возвращаемого значения, компилятор не сможет напрямую использовать адрес t1 для скрытой копии возвращаемого значения.

0 голосов
/ 27 августа 2011

Первый фрагмент создает три объекта:

  • B t1
  • B (5) <- из (int) constructor;это временный объект для функции воспроизведения </li>
  • return b;или B (b) <- copy ctor </li>

Это мое предположение, хотя это выглядит неэффективно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...