TL; DR: передача по константной ссылке все еще хорошая идея в C ++, учитывая все обстоятельства. Не преждевременная оптимизация.
TL; DR2: большинство пословиц не имеет смысла, пока они не делают.
цель
Этот ответ просто пытается немного расширить связанный элемент в C ++ Core Guidelines (впервые упоминается в комментарии amon).
Этот ответ не пытается решить вопрос о том, как правильно мыслить и применять различные пословицы, которые широко распространялись в кругах программистов, особенно вопрос о согласовании противоречивых выводов или доказательств.
применимость
Этот ответ применяется только к вызовам функций (не отсоединяемые вложенные области в одном потоке).
(Примечание.) Когда проходимые вещи могут покинуть область действия (т. Е. Иметь время жизни, которое потенциально может превышать внешнюю область), становится более важным удовлетворение потребности приложения в управлении временем жизни объекта, прежде чем что-либо еще. Обычно это требует использования ссылок, которые также способны управлять временем жизни, таких как умные указатели. Альтернативой может быть использование менеджера. Обратите внимание, что лямбда - это своего рода отрезная область; Лямбда-захваты ведут себя так, как будто имеют объем объекта. Поэтому будьте осторожны с лямбда-захватами. Также будьте осторожны с тем, как передается сама лямбда - копией или ссылкой.
Когда передавать по значению
Для значений, которые являются скалярными (стандартные примитивы, которые вписываются в машинный регистр и имеют значение семантическое), для которых нет необходимости в связи по изменчивости (совместно используемая ссылка), передайте по значению.
В ситуациях, когда вызываемый объект требует клонирования объекта или агрегата, передайте по значению, в котором копия вызываемого объекта удовлетворяет потребность в клонированном объекте.
Когда переходить по ссылке и т. Д.
во всех остальных ситуациях проходите мимо указателей, ссылок, умных указателей, дескрипторов (см .: идиома «тело-дескриптор») и т. д. Когда бы ни следовал этому совету, применяйте принцип правильной константности как обычно.
Вещи (агрегаты, объекты, массивы, структуры данных), которые занимают достаточно много места в памяти, всегда должны быть спроектированы для облегчения передачи по ссылке по соображениям производительности. Этот совет определенно применяется, когда он составляет сотни или более байтов. Этот совет является пограничным, когда он составляет десятки байтов.
Необычные парадигмы
Существуют специальные парадигмы программирования, которые намеренно копируют. Например, обработка строк, сериализация, сетевое взаимодействие, изоляция, упаковка сторонних библиотек, межпроцессное взаимодействие с разделяемой памятью и т. Д. В этих прикладных областях или парадигмах программирования данные копируются из структур в структуры или иногда перепаковываются в байтовые массивы.
Как спецификация языка влияет на этот ответ, прежде чем рассматривается вопрос оптимизации.
Sub-TL; DR Распространение ссылки не должно вызывать код; передача по const-ссылке удовлетворяет этому критерию. Однако все остальные языки удовлетворяют этому критерию без особых усилий.
(Начинающим программистам на C ++ рекомендуется полностью пропустить этот раздел.)
(Начало этого раздела частично навеяно ответом gnasher729. Однако, другой вывод сделан.)
C ++ допускает определяемые пользователем конструкторы копирования и операторы присваивания.
(Это (был) смелый выбор, который был (был) и удивительным, и прискорбным. Это определенно отклонение от сегодняшней приемлемой нормы в языковом дизайне.)
Даже если программист C ++ не определяет его, компилятор C ++ должен сгенерировать такие методы на основе принципов языка, а затем определить, нужно ли выполнять дополнительный код, кроме memcpy
. Например, class
/, struct
который содержит std::vector
член, должен иметь конструктор копирования и нетривиальный оператор присваивания.
В других языках конструкторы копирования и клонирование объектов не рекомендуется (за исключением случаев, когда это абсолютно необходимо и / или имеет смысл для семантики приложения), поскольку объекты имеют ссылочную семантику в соответствии с языковым дизайном. Эти языки обычно имеют механизм сборки мусора, основанный на достижимости вместо владения на основе области или подсчета ссылок.
Когда в C ++ (или C) передается ссылка или указатель (включая константную ссылку), программисту гарантируется, что никакой специальный код (пользовательские или сгенерированные компилятором функции) не будет выполнен, кроме распространения значения адреса (ссылка или указатель). Это - ясность поведения, с которым программисты на C ++ чувствуют себя комфортно.
Однако фоном является то, что язык C ++ неоправданно сложен, так что эта ясность поведения похожа на оазис (живую среду обитания) где-то вокруг зоны радиоактивного выпадения.
Чтобы добавить больше благословений (или оскорблений), C ++ вводит универсальные ссылки (r-значения), чтобы упростить пользовательские операторы перемещения (конструкторы перемещения и операторы назначения перемещения) с хорошей производительностью. Это выгодно для весьма актуального варианта использования (перемещение (перенос) объектов из одного экземпляра в другой) за счет уменьшения потребности в копировании и глубоком клонировании. Однако на других языках нелогично говорить о таком перемещении объектов.
(Не по теме) Раздел, посвященный статье «Хотите скорость? Передайте по значению!» написано около 2009 года.
Эта статья была написана в 2009 году и объясняет обоснование дизайна для r-значения в C ++. Эта статья представляет собой действительный контраргумент моего заключения в предыдущем разделе. Однако пример кода статьи и претензия к производительности уже давно опровергнуты.
Sub-TL; DR Конструкция семантики r-значения в C ++ допускает удивительно элегантную семантику на стороне пользователя Sort
, например, для функции. Этот элегантный невозможно моделировать (подражать) на других языках.
Функция сортировки применяется ко всей структуре данных. Как упомянуто выше, было бы медленно, если бы много копировалось. Как оптимизация производительности (это практически актуально), функция сортировки разработана так, чтобы быть разрушительной во многих языках, кроме C ++. Деструктивный означает, что целевая структура данных модифицируется для достижения цели сортировки.
В C ++ пользователь может выбрать вызов одной из двух реализаций: деструктивной с лучшей производительностью или обычной, которая не изменяет ввод. (Шаблон опущен для краткости.)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Помимо сортировки, эта элегантность также полезна при реализации алгоритма деструктивного поиска медианы в массиве (изначально не отсортированного) путем рекурсивного разбиения.
Однако обратите внимание, что в большинстве языков для сортировки применяется подход сбалансированного бинарного дерева поиска вместо применения алгоритма деструктивной сортировки к массивам. Поэтому практическая значимость этого метода не так высока, как кажется.
Как оптимизация компилятора влияет на этот ответ
Когда встраивание (а также оптимизация всей программы / оптимизация времени соединения) применяется к нескольким уровням вызовов функций, компилятор может видеть (иногда исчерпывающе) поток данных. Когда это происходит, компилятор может применить много оптимизаций, некоторые из которых могут исключить создание целых объектов в памяти. Как правило, когда эта ситуация применима, не имеет значения, переданы ли параметры по значению или по const-ссылке, потому что компилятор может провести тщательный анализ.
Однако, если функция более низкого уровня вызывает что-то, что находится за пределами анализа (например, что-то из другой библиотеки вне компиляции или граф вызовов просто слишком сложен), то компилятор должен оптимизировать защиту.
Объекты, размер которых превышает значение регистра компьютера, могут быть скопированы с помощью явных инструкций загрузки / сохранения памяти или путем вызова уважаемой memcpy
функции. На некоторых платформах компилятор генерирует SIMD-инструкции для перемещения между двумя ячейками памяти, каждая из которых перемещается на десятки байтов (16 или 32).
Обсуждение вопроса о многословии или визуальном беспорядке
Программисты C ++ привыкли к этому, то есть, если программист не ненавидит C ++, накладные расходы по написанию или чтению const-ссылки в исходном коде не являются ужасными.
Анализ затрат и выгод, возможно, проводился много раз раньше. Я не знаю, есть ли какие-нибудь научные, на которые следует ссылаться. Я думаю, что большинство анализов были бы ненаучными или невоспроизводимыми.
Вот что я представляю (без доказательств или достоверных ссылок) ...
- Да, это влияет на производительность программного обеспечения, написанного на этом языке.
- Если компиляторы могут понять назначение кода, он может быть достаточно умен, чтобы автоматизировать
- К сожалению, в языках, которые предпочитают изменчивость (в отличие от функциональной чистоты), компилятор классифицирует большинство вещей как мутировавшие, поэтому автоматическое удержание константности отклонит большинство вещей как неконстантные.
- Накладные расходы зависят от людей; люди, которые считают это чрезмерным умственным трудом, отвергли бы C ++ как жизнеспособный язык программирования.