В С++ существует ли еще плохая практика вернуть вектор из функции?

Краткая версия: Общепринято возвращать большие объекты, такие как векторы/массивы, во многих языках программирования. Является ли этот стиль приемлемым в С++ 0x, если класс имеет конструктор перемещения или программисты на С++ считают его странным/уродливым/мерзостью?

Длинная версия: В С++ 0x это все еще считается плохой формой?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

Традиционная версия будет выглядеть так:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

В новой версии значение, возвращаемое из BuildLargeVector, является rvalue, поэтому v будет построено с использованием конструктора перемещения std::vector, предполагая, что (N) RVO не выполняется.

Даже до С++ 0x первая форма часто была бы "эффективной" из-за (N) RVO. Однако (N) RVO находится по усмотрению компилятора. Теперь, когда у нас есть ссылки rvalue, гарантируется отсутствие глубокой копии.

Изменить: вопрос действительно не о оптимизации. Обе представленные формы имеют почти идентичную производительность в реальных программах. Принимая во внимание, что в прошлом первая форма могла иметь худшие показатели по порядку величины. В результате первая форма была основным запахом кода в программировании на С++ в течение длительного времени. Надеюсь, не больше?

Ответ 1

Дейв Абрахамс имеет довольно всесторонний анализ скорости передачи/возвращения значений.

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

Ответ 2

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

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

Ответ 3

Суть заключается в следующем:

Copy Elision и RVO могут избегать "страшных копий" (компилятор не требуется для реализации этих оптимизаций, и в некоторых ситуациях он не может быть применен)

Ссылки на С++ 0x RValue позволяют реализацию строк/векторов, которые гарантирует.

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

К сожалению, это имеет большое влияние на ваши интерфейсы. Если С++ 0x не является опцией, и вам нужны гарантии, вы можете использовать вместо них объекты с подсчетом ссылок или копирования на запись в некоторых сценариях. Тем не менее, у них есть недостатки с многопоточным движением.

(Я хочу, чтобы один ответ на С++ был простым и понятным и без условий).

Ответ 4

Я все еще думаю, что это плохая практика, но стоит отметить, что моя команда использует MSVC 2008 и GCC 4.1, поэтому мы не используем последние компиляторы.

Ранее многие горячие точки, показанные в vtune с MSVC 2008, дошли до строкового копирования. У нас был такой код:

String Something::id() const
{
    return valid() ? m_id: "";
}

... обратите внимание, что мы использовали собственный тип String (это было необходимо, потому что мы предоставляем комплект для разработки программного обеспечения, в котором разработчики плагинов могут использовать разные компиляторы и, следовательно, разные несовместимые реализации std::string/std:: wstring).

Я сделал простое изменение в ответ на сеанс профилирования выборки диаграммы вызовов, показывающий, что String:: String (const String &) занимает значительное количество времени. Методы, подобные приведенному выше, были наибольшими вкладчиками (на самом деле сеанс профилирования показал, что распределение памяти и освобождение являются одним из самых больших горячих точек, причем конструктор экземпляра String является основным источником для распределения).

Изменение, которое я сделал, было простым:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

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

Заключение: мы не используем абсолютные последние компиляторы, но мы по-прежнему не можем зависеть от компилятора, оптимизирующего копирование для надежного возврата (по крайней мере, не во всех случаях). Это может быть не для тех, кто использует более новые компиляторы, такие как MSVC 2010. Я с нетерпением жду, когда мы сможем использовать С++ 0x и просто будем использовать ссылки rvalue, и вам никогда не придется беспокоиться о том, что мы пессимизируем наш код, возвращая сложный классы по значению.

[Изменить] Как указал Нейт, RVO применяется к возвращаемым временным рядам, созданным внутри функции. В моем случае таких временных рядов не было (кроме недействительной ветки, где мы строим пустую строку), и поэтому RVO не применимо.

Ответ 5

Просто немного потрудиться: во многих языках программирования нередко возвращать массивы из функций. В большинстве из них возвращается ссылка на массив. В С++ ближайшая аналогия будет возвращать boost::shared_array

Ответ 6

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

Ответ 7

Действительно, поскольку С++ 11 в большинстве случаев стоимость копирования std::vector исчезает.

Однако следует иметь в виду, что стоимость создания нового вектора (тогда его разрушение) все еще существует, и использование выходных параметров вместо возврата по значению по-прежнему полезно, когда вы хотите повторно использовать векторную емкость. Это задокументировано как исключение в F.20 основных принципов С++.

Для сравнения:

std::vector<int> BuildLargeVector(int i) {
    return std::vector<int>(1000000, i);
}

int main() {
    for (int i = 0; i < 100; ++i) {
        std::vector<int> v = BuildLargeVector(i);
        // [...] do smth with v
    }
}

С

void BuildLargeVector(/*out*/ std::vector<int>& v, int i) {
    v.assign(1000000, i);
}

int main() {
    std::vector<int> v;
    for (int i = 0; i < 100; ++i) {
        BuildLargeVector(/*out*/ v, i);
        // [...] do smth with v
    }
}

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