Плохо ли писать код, основанный на оптимизации компилятора?


99

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

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

Это микрооптимизация или что-то, о чем я должен помнить при разработке своего кода?


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

4
@ Walfrat, даже если объекты довольно большие, порядка мегабайт? Мои массивы могут стать огромными из-за характера проблем, которые я решаю.
Мэтт

6
@ Мэтт, я бы не стал. Ссылки / указатели существуют именно для этого. Предполагается, что оптимизация компилятора выходит за рамки того, что программисты должны принимать во внимание при создании программы, даже если да, часто два мира пересекаются.
Нил

5
@Matt Если вы не делаете что-то чрезвычайно специфическое, что предполагает, что разработчики должны иметь 10+ опыта работы с ядрами C /, низкое аппаратное взаимодействие вам не нужно. Если вы считаете, что относитесь к чему-то очень специфическому, отредактируйте свой пост и добавьте точное описание того, что должно делать ваше приложение (в режиме реального времени? Тяжелые математические вычисления? ...)
Walfrat

37
В частном случае C ++ (N) RVO, да, полагаться на эту оптимизацию вполне допустимо. Это связано с тем, что стандарт C ++ 17 специально предписывает, чтобы это происходило в ситуациях, когда современные компиляторы уже это делали.
Caleth

Ответы:


130

Используйте принцип наименьшего удивления .

Вы и только когда-либо будете использовать этот код, и уверены ли вы в том, что вы через 3 года не удивитесь тому, что вы делаете?

Тогда иди вперед.

Во всех остальных случаях используйте стандартный способ; в противном случае вы и ваши коллеги столкнетесь с трудностями при поиске ошибок.

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


88
@ Нет, это моя точка зрения, все полагаются на оценку короткого замыкания. И вам не нужно дважды об этом думать, его нужно включить. Это стандарт де-факто. Да, вы можете изменить это, но вы не должны.
Питер Б

49
«Я изменил, как работает язык, и твой грязный гнилой код сломался! Ааааа!» Ух ты. Было бы уместно шлепать, отправьте своего коллегу на обучение дзен, там его много.

109
@PieterB Я уверен, что спецификации языка C и C ++ гарантируют оценку короткого замыкания. Так что это не только де - факто стандартом, это стандарт. Без этого вы даже больше не используете C / C ++, но что-то подозрительно на это похоже: P
marcelm

47
Просто для справки, стандартный способ здесь - возврат по значению.
DeadMG

28
@ dan04 да, это было в Delphi. Ребята, не увлекайтесь примером, о котором я говорил. Не делайте удивительных вещей, которые никто другой не делает.
Питер Б

81

Для этого конкретного случая определенно просто вернуть по значению.

  • RVO и NRVO являются хорошо известными и надежными оптимизациями, которые действительно должны быть сделаны любым достойным компилятором, даже в режиме C ++ 03.

  • Семантика перемещения гарантирует, что объекты будут удалены из функций, если (N) RVO не было. Это полезно только в том случае, если ваш объект использует динамические данные для внутреннего использования (как, например, std::vectorделает), но это действительно так, если он слишком большой - переполнение стека представляет собой риск для больших автоматических объектов.

  • C ++ 17 обеспечивает RVO. Так что не волнуйтесь, он не исчезнет и полностью завершится, как только обновятся компиляторы.

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

Просто напишите код, который имеет смысл, и поблагодарите авторов компилятора за правильную оптимизацию кода, который имеет смысл.


9
Просто ради интереса посмотрим, как Borland Turbo C ++ 3.0 1990-х годов работает с RVO . Спойлер: Это в основном работает просто отлично.
NWP

9
Ключевым моментом здесь является то, что это не какая-то случайная оптимизация для конкретного компилятора или «недокументированная функция», а то, что, хотя и является технически необязательным в нескольких версиях стандарта C ++, активно поддерживается промышленностью, и почти каждый крупный компилятор сделал это для очень долгое время.

7
Эта оптимизация не так надежна, как хотелось бы. Да, он достаточно надежен в самых очевидных случаях, но, например, в gzzilla gcc есть много едва менее очевидных случаев, когда он пропущен.
Марк

62

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

Это не какая-то малоизвестная, приятная, микрооптимизация, о которой вы читаете в небольшом блоге с небольшим трафиком, и затем вы чувствуете себя умнее и превосходнее в использовании.

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

Вы должны полностью положиться на эту оптимизацию.

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


3
Спасибо, это имеет большой смысл и соответствует «принципу наименьшего удивления», упомянутому выше. Это сделало бы код очень ясным и понятным, а также усложнило бы путаницу с указателями махинаций.
Мэтт

3
@Matt Одной из причин, по которой я проголосовал за этот ответ, является то, что в нем упоминается «семантика значений». По мере того, как вы приобретаете больше опыта в C ++ (и программировании в целом), вы обнаружите случайные ситуации, когда семантика значений не может использоваться для определенных объектов, потому что они изменчивы, и их изменения должны быть видны другому коду, использующему этот же объект ( пример "общей изменчивости"). Когда такие ситуации случаются, затронутые объекты должны быть переданы через (умные) указатели.
rwong

16

Правильность написанного вами кода никогда не должна зависеть от оптимизации. Он должен выводить правильный результат при выполнении на виртуальной машине C ++, которую они используют в спецификации.

Однако то, о чем вы говорите, это скорее вопрос эффективности. Ваш код работает лучше, если его оптимизировать с помощью оптимизирующего компилятора RVO. Это хорошо, по всем причинам, указанным в других ответах.

Однако, если вам требуется эта оптимизация (например, если конструктор копирования фактически приведет к сбою вашего кода), теперь вы находитесь в прихоти компилятора.

Я думаю, что лучший пример этого в моей собственной практике - оптимизация хвостовых вызовов:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Это глупый пример, но он показывает хвостовой вызов, где функция вызывается рекурсивно прямо в конце функции. Виртуальная машина C ++ покажет, что этот код работает должным образом, хотя я могу немного озадачиться тем, почему я вообще потрудился написать такую ​​процедуру добавления. Однако в практических реализациях C ++ у нас есть стек, и он имеет ограниченное пространство. Если сделать это педантично, эта функция должна была бы помещать по крайней мере b + 1стековые кадры в стек, поскольку она выполняет добавление. Если я хочу рассчитать sillyAdd(5, 7), это не имеет большого значения. Если я захочу рассчитать sillyAdd(0, 1000000000), у меня могут возникнуть проблемы с запуском StackOverflow (и не очень хорошим ).

Тем не менее, мы можем видеть, что как только мы дойдем до последней строки возврата, мы действительно закончим со всем в текущем кадре стека. Нам не нужно держать это вокруг. Оптимизация Tail Call позволяет вам «использовать» существующий кадр стека для следующей функции. Таким образом, нам нужен только 1 кадр стека, а не b+1. (Мы все еще должны делать все эти глупые добавления и вычитания, но они не занимают больше места.) По сути, оптимизация превращает код в:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

В некоторых языках оптимизация хвостового вызова явно требуется спецификацией. C ++ не является одним из них. Я не могу полагаться на компиляторы C ++, чтобы распознать эту возможность оптимизации хвостовых вызовов, если я не пойду в каждом конкретном случае. В моей версии Visual Studio версия выпуска выполняет оптимизацию хвостового вызова, а версия отладки - нет (по замыслу).

Таким образом, для меня было бы плохо зависеть от способности рассчитывать sillyAdd(0, 1000000000).


2
Это интересный случай, но я не думаю, что вы можете обобщить это правило в своем первом абзаце. Предположим, у меня есть программа для небольшого устройства, которая будет загружаться тогда и только тогда, когда я использую оптимизации компилятора, уменьшающие размер - это неправильно? кажется довольно педантичным сказать, что мой единственный верный выбор - переписать его на ассемблере, особенно если это переписывает то же самое, что оптимизатор, чтобы решить проблему.
sdenham

5
@ sdenham Полагаю, в споре есть немного места. Если вы больше не пишете для «C ++», а пишете для «WindRiver C ++ версии 3.4.1», тогда я могу увидеть логику там. Однако, как правило, если вы пишете что-то, что не работает должным образом в соответствии со спецификацией, вы находитесь в совершенно ином сценарии. Я знаю, что в библиотеке Boost есть подобный код, но они всегда помещают его в #ifdefблоки и имеют доступный обходной путь, соответствующий стандартам.
Корт Аммон

4
это опечатка во втором блоке кода, где написано b = b + 1?
19

2
Возможно, вы захотите объяснить, что вы подразумеваете под «виртуальной машиной C ++», поскольку этот термин не используется ни в одном стандартном документе. Я думаю, что вы говорите о модели выполнения C ++, но не совсем уверены - и ваш термин обманчиво похож на «виртуальную машину с байт-кодом», которая относится к чему-то совершенно другому.
Тоби Спейт

1
@supercat Scala также имеет явный синтаксис хвостовой рекурсии. C ++ - это свой собственный зверь, но я думаю, что хвостовая рекурсия недиоматична для нефункциональных языков и обязательна для функциональных языков, оставляя небольшой набор языков, где разумно иметь явный синтаксис хвостовой рекурсии. Буквальный перевод хвостовой рекурсии в циклы и явную мутацию просто лучший вариант для многих языков.
Просфилаес

8

На практике программы на C ++ ожидают некоторой оптимизации компилятора.

Обратите внимание на стандартные заголовки ваших стандартных реализаций контейнеров . С GCC вы можете запросить предварительно обработанную форму ( g++ -C -E) и внутреннее представление GIMPLE ( g++ -fdump-tree-gimpleили Gimple SSA с -fdump-tree-ssa) для большинства исходных файлов (технически единиц перевода) с использованием контейнеров. Вы будете удивлены количеством оптимизации, которая сделана (с g++ -O2). Таким образом, разработчики контейнеров полагаются на оптимизацию (и большую часть времени разработчик стандартной библиотеки C ++ знает, что должно произойти, и записывает реализацию контейнера с учетом этого; иногда он также записывает этап оптимизации в компиляторе для иметь дело с функциями, необходимыми для стандартной библиотеки C ++).

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

И также для случая RVO, упомянутого в вашем вопросе.

Стандарт C ++ был разработан совместно (в частности, путем экспериментов с достаточно хорошими оптимизациями, предлагая новые функции), чтобы хорошо работать с возможными оптимизациями.

Например, рассмотрим программу ниже:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

скомпилируйте это с g++ -O3 -fverbose-asm -S. Вы обнаружите, что сгенерированная функция не выполняет никаких CALLмашинных инструкций. Таким образом , большинство шагов C ++ (строительство закрытия лямбда, его многократное применение, получение beginи endитераторы, и т.д ...) были оптимизированы. Машинный код содержит только цикл (который не указан явно в исходном коде). Без такой оптимизации C ++ 11 не будет успешным.

добавлений

(добавлено 31 декабря ул 2017)

См. CppCon 2017: Мэтт Годболт «Что мой компилятор сделал для меня в последнее время? Откручиваем крышку компилятора » .


4

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

Кроме того, в тех случаях, как RVO, где это указано в языке, было бы бессмысленно изо всех сил избегать его использования, особенно если это делает исходный код проще.

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

Могут быть причины для использования неоптимизированного кода (например, для отладки), но случай, упомянутый в этом вопросе, по-видимому, не один (и если ваш код дает сбой только при оптимизации, и это не является следствием некоторой особенности устройство, на котором вы его запускаете, значит, где-то есть ошибка, и она вряд ли будет в компиляторе.)


3

Я думаю, что другие хорошо освещали специфический взгляд на C ++ и RVO. Вот более общий ответ:

Когда дело доходит до правильности, вы не должны полагаться на оптимизацию компилятора или поведение компилятора в целом. К счастью, вы, кажется, этого не делаете.

Когда дело доходит до производительности, вы должны полагаться на поведение компилятора в целом и оптимизацию компилятора в частности. Стандартно-совместимый компилятор может свободно компилировать ваш код любым удобным для него способом, если скомпилированный код ведет себя в соответствии со спецификацией языка. И я не знаю какой-либо спецификации для основного языка, которая определяет, как быстро должна выполняться каждая операция.


1

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

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

Если оптимизация компилятора приводит к другим результатам, это ошибка - ошибка компилятора. Опираясь на ошибку в компиляторе, в долгосрочной перспективе ошибка - что произойдет, когда она будет исправлена?

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


К сожалению, большая часть документации по компилятору не справляется с заданием того, что гарантировано или нет в различных режимах. Кроме того, «современные» авторы компиляторов, кажется, не обращают внимания на комбинации гарантий, которые нужны и не нужны программистам. Если программа будет работать нормально, если x*y>zпроизвольно выдаст 0 или 1 в случае переполнения, при условии, что у нее нет других побочных эффектов , требуется, чтобы программист либо предотвращал переполнения любой ценой, либо заставлял компилятор оценивать выражение определенным образом. ненужные ухудшения оптимизации против высказывания того, что ...
суперкат

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

1

Нет.

Это то, что я делаю все время. Если мне нужно получить доступ к произвольному 16-битному блоку в памяти, я делаю это

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... и полагаться на то, что компилятор сделает все возможное для оптимизации этого куска кода. Код работает на ARM, i386, AMD64 и практически на каждой отдельной архитектуре. Теоретически, неоптимизирующий компилятор может вызывать memcpy, что приводит к совершенно плохой производительности, но для меня это не проблема, так как я использую оптимизацию компилятора.

Рассмотрим альтернативу:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

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

Разница между -O2 и -O0 при использовании memcpyхитрости велика: производительность контрольной суммы IP 3,2 Гбит / с против производительности контрольной суммы IP 67 Гбит / с. Разница на порядок больше!

Иногда вам может понадобиться помощь компилятору. Так, например, вместо того, чтобы полагаться на компилятор для развертывания циклов, вы можете сделать это самостоятельно. Либо путем внедрения известного устройства Даффа , либо более чистым способом.

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

Что бы вы ни делали, пожалуйста, убедитесь, что ваш путь на самом деле не неопределенное поведение. Конечно, доступ к некоторому случайному блоку памяти в виде 16-разрядного целого числа является неопределенным поведением из-за проблем с псевдонимами и выравниванием.


0

Все попытки эффективного кода, написанного на чем угодно, кроме ассемблера, очень и очень сильно зависят от оптимизации компилятора, начиная с самого простого, такого как эффективное распределение регистров, чтобы избежать лишних разливов стека повсюду и, по крайней мере, достаточно хорошего, если не отличного, выбора команд. В противном случае мы вернулись бы в 80-е годы, когда нам приходилось ставить registerподсказки повсеместно и использовать минимальное количество переменных в функции, чтобы помочь архаичным компиляторам Си или даже раньше, когда gotoбыла полезна оптимизация ветвления.

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

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

RVO - это то, что существует уже много лет, и, по крайней мере, за исключением очень сложных случаев, это то, что компиляторы надежно применяют на протяжении веков. Определенно не стоит обходить проблему, которой не существует.

Ошибка на стороне полагаться на оптимизатор, не опасаясь его

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

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

профилирование

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


-1

Программное обеспечение может быть написано на C ++ для самых разных платформ и для множества разных целей.

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


-2

Я думаю, что скучный ответ на это: «это зависит».

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

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


-6

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

Возможно, в отличие от других языков, которые вы использовали ранее, возвращая значение объекта в C ++, вы получаете копию объекта. Если вы затем модифицируете объект, вы модифицируете другой объект . Это если у меня естьObj a; a.x=1; а Obj b = a;затем я делаю b.x += 2; b.f();, то a.xвсе равно равно 1, а не 3.

Поэтому нет, использование объекта в качестве значения вместо ссылки или указателя не обеспечивает такую ​​же функциональность, и вы можете получить ошибки в программном обеспечении.

Возможно, вы знаете это, и это не оказывает негативного влияния на ваш конкретный вариант использования. Однако, исходя из формулировки в вашем вопросе, кажется, что вы можете не знать о различии; формулировка, такая как «создать объект в функции».

«создать объект в функции» звучит как new Obj; где «вернуть объект по значению» звучит какObj a; return a;

Obj a;и Obj* a = new Obj;очень, очень разные вещи; первый может привести к повреждению памяти, если он не используется должным образом и не понят, а последний может привести к утечке памяти, если не используется и не используется должным образом.


8
Оптимизация возвращаемого значения (RVO) - это четко определенная семантика, в которой компилятор создает возвращаемый объект на один уровень выше в кадре стека, в частности избегая ненужных копий объекта. Это четко определенное поведение, которое поддерживалось задолго до того, как оно было предписано в C ++ 17. Еще 10-15 лет назад все основные компиляторы поддерживали эту функцию и делали это последовательно.

@ Снеговик Я не говорю о физическом низкоуровневом управлении памятью, и я не обсуждал раздувание или скорость памяти. Как я конкретно показал в своем ответе, я говорю о логических данных. Логически , предоставление значения объекта создает его копию, независимо от того, как реализован компилятор или какая сборка используется за кулисами. Закулисные низкоуровневые вещи - это одно, а логическая структура и поведение языка - это другое; они связаны, но это не одно и то же - оба должны быть поняты.
Аарон

6
в вашем ответе говорится, что «возвращая значение объекта в C ++, вы получаете копию объекта», что совершенно неверно в контексте RVO - объект создается непосредственно в месте вызова, и никакая копия не создается. Вы можете проверить это, удалив конструктор копирования и вернув объект ,return созданный в операторе, который является требованием для RVO. Кроме того, вы продолжаете говорить о ключевых newсловах и указателях, а это не то, чем занимается RVO. Я полагаю, вы либо не поняли вопрос, либо RVO, либо, возможно, оба.

-7

Питер Б абсолютно прав, рекомендуя наименьшее изумление.

Чтобы ответить на ваш конкретный вопрос, что это (наиболее вероятно) означает в C ++, что вы должны вернуть a std::unique_ptrк созданному объекту.

Причина в том, что для разработчика на C ++ это более понятно, что происходит.

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

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


Когда в Риме, делай, как римляне.

14
Это не очень хороший ответ для типов, которые сами не выполняют динамическое распределение. То, что OP чувствует себя естественным в его случае использования, состоит в том, что возвращение по значению указывает на то, что его объекты автоматически хранятся на стороне вызывающей стороны. Для простых, не слишком больших объектов даже наивная реализация с возвращаемым значением будет на несколько порядков быстрее, чем динамическое распределение. (Если, с другой стороны, функция возвращает контейнер, то возвращение unique_pointer может быть даже
Питер А. Шнайдер

9
@ Matt В случае, если вы не поняли, это не лучшая практика. Излишне делать выделение памяти и навязывать семантику указателей пользователям плохо.
NWP

5
Прежде всего, при использовании умных указателей нужно возвращаться std::make_unique, а не std::unique_ptrнапрямую. Во-вторых, RVO - это не какая-то эзотерическая, специфичная для поставщика оптимизация: она встроена в стандарт. Даже тогда, когда этого не было, его широко поддерживали и ожидали поведения. Нет смысла возвращать, std::unique_ptrкогда указатель не нужен в первую очередь.

4
@ Снеговик: нет "когда это не было". Хотя это только недавно стало обязательным , каждый стандарт C ++ когда-либо распознавал [N] RVO и делал приспособления для его включения (например, компилятору всегда давалось явное разрешение на использование конструктора копирования в возвращаемом значении, даже если он имеет видимые побочные эффекты).
Джерри Гроб
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.