Почему предпочтительно писать func (const Class & value)? - PullRequest
4 голосов
/ 02 ноября 2009

Зачем использовать func( const Class &value ), а не просто func( Class value )? Конечно, современные компиляторы будут делать самые эффективные вещи, используя любой синтаксис. Это все еще необходимо или это просто время, оставшееся со времен неоптимизирующих компиляторов?

  • Просто добавим, что gcc будет выводить аналогичный код ассемблера для любого синтаксиса. Возможно, других компиляторов нет?

По-видимому, это просто не тот случай. Из какого-то кода у меня давно создалось впечатление, что это делает gcc, но эксперименты доказывают, что это неправильно. Благодарность принадлежит Майклу Берру, чей ответ на подобный вопрос будет выдвинут, если он будет приведен здесь.

Ответы [ 9 ]

17 голосов
/ 02 ноября 2009

Между двумя сигнатурами есть 2 больших семантических различия.

Первое - это использование & в имени типа. Это сигнализирует, что значение передается по ссылке. Удаление этого приводит к тому, что объект передается по значению, которое, по существу, передает копию объекта в функцию (через конструктор копирования). Для операций, которые просто должны прочитать данные (типично для const &), выполнение полной копии объекта создает ненужные накладные расходы. Для классов, которые не являются маленькими или являются коллекциями, эти издержки не являются тривиальными.

Вторым является использование const. Это предотвращает случайное изменение функции значения содержимым через ссылку value. Это позволяет вызывающей стороне быть уверенным, что функция не будет видоизменена. Да, передача копии дает звонящему гораздо более глубокую уверенность в этом во многих случаях.

8 голосов
/ 02 ноября 2009

Существует огромная разница, о которой еще никто не упомянул: разрезание объектов. В некоторых случаях вам может потребоваться const& (или &), чтобы получить правильное поведение.

Рассмотрим другой класс Derived, который наследуется от Class. В клиентском коде вы создаете экземпляр Derived, который вы передаете func(). Если у вас есть func(const Class&), тот же экземпляр будет передан. Как уже говорили другие, func(Class) сделает копию, у вас будет новый (временный) экземпляр Class (не Derived) в func.

Эта разница в поведении (а не в производительности) может быть важной, если func в свою очередь делает удручение. Сравните результаты выполнения следующего кода:

#include <typeinfo.h>

struct Class
{
    virtual void Foo() {};
};
class Derived : public Class {};

void f(const Class& value)
{
    printf("f()\n");

    try
    {
        const Derived& d = dynamic_cast<const Derived&>(value);
        printf("dynamic_cast<>\n");
    }
    catch (std::bad_cast)
    {
        fprintf(stderr, "bad_cast\n");
    }
}

void g(Class value)
{
    printf("g()\n");

    try
    {
        const Derived& d = dynamic_cast<const Derived&>(value);
        printf("dynamic_cast<>\n");
    }
    catch (std::bad_cast)
    {
        fprintf(stderr, "bad_cast\n");
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    Derived d;
    f(d);
    g(d);
    return 0;
}
8 голосов
/ 02 ноября 2009

Первая форма не создает копию объекта, она просто передает ссылку (указатель) на существующую копию. Вторая форма создает копию, которая может быть дорогой. Это не то, что оптимизировано: существуют семантические различия между наличием копии объекта и оригинала, а копирование требует вызова конструктора копирования класса.

Для очень маленьких классов (например, <16 байт) без конструктора копирования, вероятно, более эффективно использовать синтаксис значения, а не передавать ссылки. Вот почему вы видите <code>void foo(double bar), а не void foo(const double &var). Но в интересах не микрооптимизирующего кода, который не имеет значения, как правило, вы должны передавать все объекты реальных сделок по ссылке и только встроенные типы, такие как int и void * по значению.

4 голосов
/ 02 ноября 2009

Конечно, современные компиляторы сделают самая эффективная вещь, используя либо Синтаксис

Компилятор не компилирует то, что вы "имеете в виду", он компилирует то, что вы говорите . Компиляторы умны только для оптимизации низкого уровня и проблем, которые пропускает программист (например, вычисления внутри цикла for, мертвый код и т. Д.).

То, что вы велите компилятору делать во втором примере, - это сделать копию класса - что он будет делать, не задумываясь - даже если вы его не использовали, это то, что вы просили компилятор сделать *. 1009 *

Второй пример явно просит компилятор использовать одну и ту же переменную - экономя пространство и драгоценные циклы (копирование не требуется). Const существует для ошибок - так как Class &value может быть записано (иногда это желательно).

3 голосов
/ 02 ноября 2009

Вот различия между некоторыми объявлениями параметров:

                           copied  out  modifiable
func(Class value)           Y       N    Y
func(const Class value)     Y       N    N
func(Class &value)          N       Y    Y
func(const Class &value)    N       N    N

где:

  • скопировано: копия входного параметра выполняется при вызове функции
  • out: значение является параметром "out", что означает, что изменения, сделанные в функции func (), будут видны за пределами функции после ее возврата
  • модифицируемый: значение может быть изменено в функции ()

Итак, различия между func(Class value) и func(const Class &value):

  • Первый создает копию входного параметра (вызывая конструктор копирования класса) и позволяет коду внутри func () изменять value
  • Второй не делает копию, а не позволяет коду внутри func () изменять value
1 голос
/ 02 ноября 2009

Причиной того, что оптимизирующий компилятор не может справиться с этим, является проблема отдельной компиляции. В C ++, когда компилятор генерирует код для вызывающей стороны, он может не иметь доступа к коду самой функции. Наиболее распространенное соглашение о вызовах, о котором я знаю, обычно заключается в том, что вызывающая сторона вызывает конструктор копирования, что означает, что компиляция самой функции не может предотвратить конструктор копирования, если в этом нет необходимости.

1 голос
/ 02 ноября 2009

Первый пример - передача по ссылке. Вместо того, чтобы передавать тип, C ++ будет передавать ссылку на объект (обычно ссылки реализуются с помощью указателей ... Таким образом, это, вероятно, объект размером 4 байта) ... Во втором примере объект передается по значению. .. если это большой, сложный объект, то, скорее всего, это довольно тяжелая операция, так как она включает в себя создание копии нового «Класса».

1 голос
/ 02 ноября 2009

Если вы используете первое, а затем попытаетесь изменить value, случайно, компилятор выдаст вам ошибку.

Если вы используете последнее, а затем попытаетесь изменить value, оно не будет.

Таким образом, первое облегчает выявление ошибок.

0 голосов
/ 02 ноября 2009

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

std::string toUpper( const std::string &value ) {
    std::string retVal(value);
    transform(retVal.begin(), retVal.end(), charToUpper());
    return retVal;
}

Или

std::string toUpper( std::string value ) {
    transform(value.begin(), value.end(), charToUpper());
    return value;
}

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

Хотя большинство компиляторов уже выполнят эту оптимизацию, я не собираюсь полагаться на эту функцию до C ++ 0X, особенно потому, что я ожидаю, что это может смутить большинство программистов, которые, вероятно, вернут ее обратно.

См. Хотите скорость? Передайте по значению. для лучшего объяснения, чем я мог бы дать.

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