Подтвердить что ты не робот

Должен ли я вернуть ссылочный параметр rvalue по ссылке rvalue?

У меня есть функция, которая изменяет std::string& lvalue ссылки на месте, возвращая ссылку на входной параметр:

std::string& transform(std::string& input)
{
    // transform the input string
    ...

    return input;
}

У меня есть вспомогательная функция, которая позволяет выполнять одни и те же встроенные преобразования на ссылках rvalue:

std::string&& transform(std::string&& input)
{
    return std::move(transform(input)); // calls the lvalue reference version
}

Обратите внимание, что возвращает ссылку rvalue.

Я прочитал несколько вопросов по SO, связанных с возвратом ссылок rvalue (здесь и here для пример), и пришли к выводу, что это плохая практика.

Из того, что я прочитал, кажется, что консенсус в том, что, поскольку значения return являются r плюс, с учетом RVO, просто возврат по значению будет таким же эффективным:

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

Однако я также прочитал, что возвращаемые функциональные параметры предотвращают оптимизацию RVO (например здесь и here)

Это заставляет меня думать, что копия произойдет из возвращаемого значения std::string& ссылочной версии lvalue transform(...) в возвращаемое значение std::string.

Правильно ли это?

Лучше ли сохранить версию std::string&& transform(...)?

4b9b3361

Ответ 1

Нет правильного ответа, но возврат по значению более безопасен.

Я прочитал несколько вопросов о SO, связанных с возвратом ссылок на rvalue, и пришел к выводу, что это плохая практика.

Возврат ссылки на параметр налагает контракт на вызывающего абонента, что либо

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

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

Из того, что я прочитал, кажется, что консенсус заключается в том, что поскольку значения возврата являются значениями r плюс, а также с учетом RVO, просто возврат по значению будет таким же эффективным:

Возврат по значению добавляет операцию перемещения. Стоимость этого обычно пропорциональна размеру объекта. Если для возврата по ссылке требуется только устройство, чтобы гарантировать, что один адрес находится в регистре, для возврата по значению требуется обнулить пару указателей в параметре std::string и поместить их значения в новый std::string, который будет возвращен.

Это дешево, но отличное от нуля.

Направление, в настоящее время принятое стандартной библиотекой, несколько удивительно, чтобы быть быстрым и небезопасным и возвращать ссылку. (Единственная функция, которую я знаю, на самом деле это std::get от <tuple>.) Как это случается, я представил предложение в основной язык С++ к решению этой проблемы, версия находится в работе, и только сегодня я начал исследовать реализацию. Но это осложнилось, и не обязательно.

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

Компилятор здесь не будет генерировать move. Если input не было ссылкой вообще, и вы сделали return input;, но это не имеет оснований полагать, что transform вернет input только потому, что это параметр, и он не будет выводить владение от rvalue ссылка type в любом случае. (См. С++ 14 §12.8/31-32.)

Вам нужно сделать:

return std::move( transform( input ) );

или эквивалентно

transform( input );
return std::move( input );

Ответ 2

Некоторые (нерепрезентативные) времена выполнения для вышеперечисленных версий transform:

запустить на coliru

#include <iostream>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>

using namespace std;

double GetTicks()
{
    struct timeval tv;
    if(!gettimeofday (&tv, NULL))
        return (tv.tv_sec*1000 + tv.tv_usec/1000);
    else
        return -1;
}

std::string& transform(std::string& input)
{
    // transform the input string
    // e.g toggle first character
    if(!input.empty())
    {
        if(input[0]=='A')
            input[0] = 'B';
        else
            input[0] = 'A';
    }
    return input;
}

std::string&& transformA(std::string&& input)
{
    return std::move(transform(input));
}

std::string transformB(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

std::string transformC(std::string&& input)
{
    return std::move( transform( input ) ); // calls the lvalue reference version
}


string getSomeString()
{
    return string("ABC");
}

int main()
{
    const int MAX_LOOPS = 5000000;

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformA(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformA: " << end - start << " ms" << endl;
    }

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformB(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformB: " << end - start << " ms" << endl;
    }

    {
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformC(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformC: " << end - start << " ms" << endl;
    }

    return 0;
}

Выход

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

Runtime transformA: 444 ms
Runtime transformB: 796 ms
Runtime transformC: 434 ms

Ответ 3

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

в основном, семантика перемещения может принести вам пользу в некоторых эзотерических случаях. скажем, у меня есть объекты-матрицы, которые содержат double** как переменную-член, и этот указатель указывает на два размерных массива double. теперь позвольте сказать, что у меня есть это выражение:
Matrix a = b+c;
конструктор копирования (или оператор присваивания в этом случае) получит сумму b и c как временный, передаст его как константную ссылку, перераспределяет m*n количество doubles на a внутреннем указатель, то он будет работать на a+b sum-array и будет копировать его значения один за другим. простое вычисление показывает, что оно может занимать до O(nm) шагов (которые могут быть сгенерированы до O(n^2)). переместить семантику будет только перезагрузить скрытый double** из temprary в внутренний указатель a. он принимает O(1).
теперь подумайте о std::string на мгновение: передавая его в качестве справки принимает шаги O(1) (берут память, меняют ее, разыгрывают и т.д., это не является линейным в любом виде). передавая его как r-value-reference, требуется, чтобы программа передавала его в качестве ссылки, повторно проложите скрытый скрытый C- char*, который содержит внутренний буфер, нулевой исходный (или обмен между ними), скопируйте size и capacity и еще много действий. мы можем видеть, что, хотя мы все еще в зоне O(1), может быть актуально БОЛЬШЕ шагов, чем просто передать его как регулярную ссылку.

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

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

Ответ 4

Это заставляет меня думать, что копия произойдет из std::string & возвращаемое значение ссылочной версии lvalue преобразования (...) в возвращаемое значение std::string.

Правильно ли это?

Обратная ссылочная версия не позволит выполнить std::string копию, но версия возвращаемого значения будет иметь копию, если компилятор не выполняет RVO. Однако у RVO есть свое ограничение, поэтому С++ 11 добавляет ссылку на r-значение и перемещает конструктор/назначение/std:: move, чтобы справиться с этой ситуацией. Да, RVO более эффективен, чем перемещение семантики, перемещение дешевле, чем копирование, но более дорогое, чем RVO.

Лучше ли поддерживать мой std::string && преобразовать (...) версию?

Это как-то интересно и странно. Как ответил Potatoswatter,

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
} 

Вы должны вызвать std:: move вручную.

Однако вы можете нажать эту ссылку для разработчиков: RVO V.S. std:: move, чтобы узнать подробности, которые ясно объясняют вашу проблему.