Передавать по значению или ссылке в конструктор C ++, которому необходимо сохранить копию?

Должен ли конструктор значений C ++ (неявный или явный) принимать свои параметры по значению или по ссылке на константу, когда ему в любом случае необходимо сохранить копию аргумента (ов) в своем объекте?

Вот самый короткий пример, который я могу придумать:

struct foo {
    bar _b;
    foo(bar [const&] b) // pass by value or reference-to-const?
        : _b(b) { }
};

Идея здесь в том, что я хочу минимизировать вызовы конструктора копирования bar при создании объекта foo любым из различных способов, которыми может быть создан объект foo.

Обратите внимание, что я немного знаю о копировании и оптимизации (именованных) возвращаемых значений, и я прочитал " Хотите скорость? Передавать по значению ", однако я не думаю, что статья напрямую касается этого варианта использования.

Изменить: я должен быть более конкретным.

Предположим, что я не могу знать sizeof(bar) и является ли bar фундаментальным встроенным типом (bar может быть параметром шаблона, а foo может быть шаблоном класса вместо класса). Кроме того, не предполагайте, что конструктор foo может быть встроенным (или bar, если на то пошло). Предполагаю, что я, по крайней мере, мог бы использовать компилятор, реализующий RVO.

Я бы хотел, чтобы была возможность (с учетом оптимизации компилятора), что такой вызов вообще не будет вызывать никаких вызовов конструктора копирования bar (даже при выполнении _b(b) в списке инициализации foo):

foo f = function_that_creates_and_returns_a_bar_object_using_rvo();

Есть ли возможность (учитывая стандарт C ++ 98), что это можно сделать, и если да, то будет ли это с большей или меньшей вероятностью работать, если foo принимает свой параметр по ссылке на const, а не по значению?


person Kurt Glastetter    schedule 04.12.2009    source источник


Ответы (8)


В C ++ 98 и C ++ 03 вы должны передать const& bar, а затем скопировать. В C ++ 0x вы должны передать bar, а затем выполнить перемещение (при условии, что bar имеет конструктор перемещения).

#include <utility>

struct foo
{
    bar _b;

    foo(bar b) : _b(std::move(b)) {}
};

Если вы создаете foo с параметром lvalue, конструктор копирования будет вызван для создания копии b, и эта копия будет перемещена в _b. Если вы создаете foo с параметром rvalue, будет вызван конструктор перемещения bar для перемещения в b, а затем он снова будет перемещен в _b.

person Fred    schedule 04.12.2009
comment
Я считаю это ответом, потому что он наиболее перспективен и показывает, как это недостаток C ++ 98/03, который будет устранен с помощью C ++ 0x. Однако см. Ответ Алексея Малистова (и следующие комментарии) для случаев, когда вы также можете передать значение для C ++ 98/03. - person Kurt Glastetter; 09.12.2009

При прочих равных я прохожу через const & для достаточно сложных классов и value для POD и простых объектов.

Выделение плюсов и минусов для передачи по константной ссылке вместо традиционной передачи по значению

Положительные:

  • Избегает копирования (большой плюс для объектов с дорогой копией)
  • Доступ только для чтения

Минус:

  • Кто-то может удалить константу из ссылки, если они действительно хотят

Что еще более важно с положительными моментами, вы явно контролируете, когда происходит копирование (в вашем случае при передаче инициализации _b в вашем списке инициализаторов). Думая о негативе ... Согласен, это риск. Я думаю, что почти все хорошие программисты почувствуют себя грязными при использовании const_cast. Более того, вы можете послушно искать const_cast и бросать помидоры в человека, отбрасывающего константу из аргумента. Но эй, как знать, а у кого есть время смотреть код как ястреб :)?

Мое субъективное мнение таково, что в достаточно сложных классах и в средах, где производительность имеет значение, выгода от отказа от конструктора копирования перевешивает риск. Однако для действительно глупых классов и POD я обычно делаю копии данных и передаю их по значению.

person Doug T.    schedule 04.12.2009

Посмотрите этот вопрос.

Используйте const T & arg, если sizeof(T)>sizeof(void*), и T arg, если sizeof(T) <= sizeof(void*). Все основные типы должны быть исключением из этого правила.

person Alexey Malistov    schedule 04.12.2009
comment
Хотя я думаю, что это хорошее практическое правило, оно, похоже, противоречит руководству, представленному в указанной статье (по крайней мере, иногда) ... И в моем конкретном случае использования foo - это шаблон класса с bar в качестве параметра шаблона, поэтому я не знаю sizeof (bar) (возможно, мне следовало включить это в свой пример :(). - person Kurt Glastetter; 04.12.2009
comment
Используйте характеристики типа, чтобы выбрать ссылку или значение в зависимости от sizeof (bar) - person Alexey Malistov; 04.12.2009
comment
@Alexey: кроме функторов и итераторов (согласно Мейерсу), которые всегда должны передаваться по значению, и мы надеемся, что компилятор поступит правильно. Однако не уверен, что курил Мейерс, чтобы прийти к такому выводу. - person Konrad Rudolph; 04.12.2009
comment
@Alexey: Хорошее замечание о чертах типа. Я вижу, где признаки типа могут предоставить что-то вроде value_or_const_ref ‹bar› :: type, что может привести к bar, если bar является встроенным типом, или если sizeof (bar) ‹= sizeof (void *), или, возможно, если sizeof ( bar) ‹= размер любого из встроенных типов, иначе результат будет const bar &. (И это опровергает мою предыдущую мысль о том, что я не могу предположить, что знаю sizeof (bar).) - person Kurt Glastetter; 09.12.2009

Стилистически я бы сказал, что передача по ссылке - лучший способ.

Если производительность действительно имеет значение, не гадайте. Измерьте это.

person Kristopher Johnson    schedule 04.12.2009
comment
+1 за рекомендацию измерить производительность вместо предположений. - person Void; 04.12.2009

Я не проверял, что говорится в стандарте, но, попробовав это эмпирически, могу сказать вам, что GCC не оптимизирует копию, независимо от того, принимает ли конструктор аргумент по значению или по константной ссылке.

Однако, если конструктор принимает ссылку на константу, ему удается избежать ненужной копии при создании foo из существующего объекта bar.

Обобщить:

Bar b = makeBar();         // No copy, because of RVO
FooByValue f1 = makeBar(); // Copy constructor of Bar called once
FooByRef f2 = makeBar();   // Copy constructor of Bar called once
FooByValue f3(b);          // Copy constructor of Bar called twice
FooByRef f4(b);            // Copy constructor of Bar called only once

Не то чтобы я являюсь экспертом по компиляторам, но, думаю, имеет смысл, что обычно он не может преобразовать возвращаемое значение в произвольное место (например, в поле member объекта foo). Вместо этого требуется, чтобы цель находилась наверху стека.

person Josef Grahn    schedule 04.12.2009

Я предполагаю, что bar имеет нормальный конструктор копирования в форме bar(const bar &b)

Возьмите здесь константную ссылку. Затем конструктор bar возьмет эту ссылку и выполнит копирование. Всего копий: 1.

Если вы не указали ссылку, компилятор сделает копию b, передаст копию конструктору foo, который затем передаст ее bar и скопирует.

person Thanatos    schedule 04.12.2009

Честно говоря, для этого случая лучшим вариантом является создание вашего конструктора inline при условии, что конструктор bar не генерирует. Тогда просто пройдите по ссылке.

Кроме того, как вы и подозревали, ваша статья здесь не применяется.

person rlbond    schedule 04.12.2009

Удерживайте shared_pointer, чтобы заблокировать в своем классе, и передайте его как таковой. Таким образом, вы никогда не вызовете конструктор копирования :).

person Patrick    schedule 04.12.2009
comment
Это очень легкий вес ... беспокойство об этом было бы случаем преждевременной оптимизации, если бы я когда-либо о ней слышал. Я просто пытался предложить другой подход :) - person Patrick; 07.12.2009