Int vs const int &

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

Как вы думаете? Стоит ли писать const int & над int? Я думаю, что он оптимизирован компилятором в любом случае, так что, может быть, я просто трачу время на его кодирование,?

Ответ 1

В C++ очень часто встречается то, что я считаю анти-паттерном, использующим const T& как умный способ просто сказать T при работе с параметрами. Однако значение и ссылка (независимо от того, является ли она константной или нет) - это две совершенно разные вещи, и всегда и вслепую использование ссылок вместо значений может привести к незначительным ошибкам.

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

Так же, как пример, одним из мест, где применяется этот анти-шаблон, является сама стандартная библиотека, где std::vector<T>::push_back принимает в качестве параметра const T& вместо значения, и это может привести, например, в коде:

std::vector<T> v;
...
if (v.size())
    v.push_back(v[0]); // Add first element also as last element

Этот код является тикающей бомбой, потому что std::vector::push_back хочет ссылку на const, но выполнение push_back может потребовать перераспределения, и если это произойдет, это означает, что после перераспределения полученная ссылка больше не будет действительной (проблема времени жизни), и вы войдите в область неопределенного поведения.

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

struct P2d
{ 
    double x, y;
    P2d(double x, double y) : x(x), y(y) {}
    P2d& operator+=(const P2d& p) { x+=p.x; y+=p.y; return *this; }
    P2d& operator-=(const P2d& p) { x-=p.x; y-=p.y; return *this; }
};

struct Rect
{
    P2d tl, br;
    Rect(const P2d& tl, const P2d& br) : tl(tl), bt(br) {}
    Rect& operator+=(const P2d& p) { tl+=p; br+=p; return *this; }
    Rect& operator-=(const P2d& p) { tl-=p; br-=p; return *this; }
};

На первый взгляд код кажется довольно безопасным, P2d - это двумерная точка, Rect - это прямоугольник, а добавление/вычитание точки означает перевод прямоугольника.

Однако, если вы хотите перевести прямоугольник обратно в начало координат, напишите myrect -= myrect.tl; код не будет работать, потому что оператор перевода был определен, принимая ссылку, которая (в этом случае) ссылается на член того же экземпляра.

Это означает, что после обновления topleft с tl -= p; topleft будет (0, 0) как и должно быть, но также p станет одновременно (0, 0) потому что p - это просто ссылка на левый верхний член, и поэтому обновление нижнего правого угла не будет работать потому что он будет переводить его как (0, 0) следовательно, ничего не делая.

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

В const T& слово const выражает свойство ссылки, а не ссылочного объекта: это свойство, которое делает невозможным его использование для изменения объекта. Вероятно, readonly было бы лучшим именем, поскольку const имеет IMO психологический эффект от идеи, что объект будет постоянным, пока вы используете ссылку.

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

Также ссылка на const всегда будет означать проблемы для оптимизатора, так как компилятор вынужден быть параноиком, и каждый раз, когда выполняется любой неизвестный код, он должен предполагать, что все ссылочные объекты могут теперь иметь другое значение (const для ссылки означает абсолютно НИЧЕГО для оптимизатор, это слово только для того, чтобы помочь программистам - лично я не уверен, что это такая большая помощь, но это другая история).

Ответ 2

Как говорит Оли, возвращение const T& в отличие от T - это совершенно разные вещи и может нарушаться в определенных ситуациях (как в его примере).

Принимая const T& в отличие от обычного T, поскольку аргумент менее правдоподобен для разрыва вещей, но все еще имеет несколько важных отличий.

  • Взятие T вместо const T& требует, чтобы T был возможно для копирования.
  • Взятие T вызовет конструктор копирования, который может быть дорогим (а также деструктором при выходе функции).
  • Взятие T позволяет вам изменять параметр как локальную переменную (может быть быстрее, чем ручное копирование).
  • Взятие const T& может быть медленнее из-за неверных временных рядов и стоимости косвенности.

Ответ 3

int & и int не взаимозаменяемы! В частности, если вы возвращаете ссылку на локальную переменную стека, поведение undefined, например:

int &func()
{
    int x = 42;
    return x;
}

Вы можете вернуть ссылку на то, что не будет уничтожено в конце функции (например, статический или член класса). Так что это действительно:

int &func()
{
    static int x = 42;
    return x;
}

и внешнему миру, имеет тот же эффект, что и возврат int (за исключением того, что теперь вы можете его изменить, поэтому вы часто видите const int &).

Преимущество ссылки заключается в том, что копия не требуется, что важно, если вы имеете дело с объектами большого класса. Однако во многих случаях компилятор может оптимизировать это; см., например, http://en.wikipedia.org/wiki/Return_value_optimization.

Ответ 4

Если вызываемый и вызывающий определяются в отдельных единицах компиляции, компилятор не может оптимизировать ссылку. Например, я скомпилировал следующий код:

#include <ctime>
#include <iostream>

int test1(int i);
int test2(const int& i);

int main() {
  int i = std::time(0);
  int j = test1(i);
  int k = test2(i);
  std::cout << j + k << std::endl;
}

с g++ в 64-разрядной Linux на уровне оптимизации 3. Первому вызову не требуется доступ к основной памяти:

call    time
movl    %eax, %edi     #1
movl    %eax, 12(%rsp) #2
call    _Z5test1i
leaq    12(%rsp), %rdi #3
movl    %eax, %ebx
call    _Z5test2RKi

Строка # 1 прямо использует возвращаемое значение в eax как аргумент для test1 в edi. Строки №2 и №3 выталкивают результат в основную память и помещают адрес в первый аргумент, потому что аргумент объявляется как ссылка на int, и поэтому он должен быть доступен, например, принять его адрес. Может ли что-то быть полностью рассчитано с использованием регистров или для доступа к основной памяти, может иметь большое значение в наши дни. Таким образом, кроме того, что больше типа, const int& также может быть медленнее. Эмпирическое правило - передать все данные, которые не превышают размер слова по значению, и все остальное по ссылке на const. Также передайте шаблонные аргументы ссылкой на const; поскольку компилятор имеет доступ к определению шаблона, он всегда может оптимизировать ссылку.

Ответ 5

Вместо "размышления", оптимизированного компилятором, почему бы вам не получить список ассемблера и не узнать наверняка?

junk.С++:

int my_int()
{
    static int v = 5;
    return v;
}

const int& my_int_ref()
{
    static int v = 5;
    return v;
}

Сгенерированный выход ассемблера (разрешен):

_Z6my_intv:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    $5, %eax
    ret
    .cfi_endproc

...

_Z10my_int_refv:
.LFB1:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    $_ZZ10my_int_refvE1v, %eax
    ret

Инструкции movl в обоих случаях очень разные. Первый перемещает 5 в EAX (который является регистром, традиционно используемым для возврата значений в код x86 C), а второй перемещает адрес переменной (специфика, исключаемый для ясности) в EAX. Это означает, что вызывающая функция в первом случае может просто использовать операции регистров, не нажимая на память, чтобы использовать ответ, а во втором он должен попасть в память через возвращаемый указатель.

Итак, похоже, что он не оптимизирован.

Это больше, чем другие ответы, которые вам даны здесь, объясняя, почему T и const T& не являются взаимозаменяемыми.

Ответ 6

int отличается от const int &:

  1. const int & является ссылкой на другую целочисленную переменную (int B), что означает: если мы изменим int B, значение const int & также изменится.

2, int является копией значения другой целочисленной переменной (int B), что означает: если мы изменим int B, значение int не изменится.

Смотрите следующий код c++:

int main() {

вектор а {1,2,3};

int b = a [2];//значение не изменяется даже при изменении вектора

const int & c = a [2];//это ссылка, поэтому значение зависит от вектора;

а [2] = 111;

//b выведет 3;

//c выдаст 111;

}