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

Ссылки на С++ 0x rvalue - привязка lvalues-rvalue

Это следующий вопрос: С++ 0x rvalue ссылки и временные файлы

В предыдущем вопросе я спросил, как должен работать этот код:

void f(const std::string &); //less efficient
void f(std::string &&); //more efficient

void g(const char * arg)
{
    f(arg);
}

Кажется, что перегрузка перемещения должна, вероятно, вызываться из-за неявного временного действия, и это происходит в GCC, но не в MSVC (или в интерфейсе EDG, используемом в MSVC Intellisense).

Как насчет этого кода?

void f(std::string &&); //NB: No const string & overload supplied

void g1(const char * arg)
{
     f(arg);
}
void g2(const std::string & arg)
{
    f(arg);
}

Кажется, что на основе ответов на мой предыдущий вопрос, что функция g1 является законной (и принимается GCC 4.3-4.5, но не MSVC). Однако GCC и MSVC отклоняют g2 из-за положения 13.3.3.1.4/3, который запрещает lvalues ​​от привязки к аргументам refvalue. Я понимаю обоснование этого - объясняется в N2831 "Устранение проблемы безопасности с помощью ссылок rvalue". Я также думаю, что GCC, вероятно, реализует этот пункт, как это было предусмотрено авторами этой статьи, потому что исходный патч для GCC был написан одним из авторов (Doug Gregor).

Однако я не уверен в этом. Для меня (a) a const string & концептуально ближе к string &&, чем a const char *, и (b) компилятор может создать временную строку в g2, как если бы она была написана следующим образом:

void g2(const std::string & arg)
{
    f(std::string(arg));
}

Действительно, иногда конструктор копирования считается неявным оператором преобразования. Синтаксически это предлагается формой конструктора копирования, и стандарт даже упоминает это конкретно в разделе 13.3.3.1.2/4, где конструктор копирования для преобразований с производной базой получает более высокий ранг преобразования, чем другие определяемые пользователем преобразования:

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

(Я предполагаю, что это используется при передаче производного класса на функцию типа void h(Base), которая берет базовый класс по значению.)

Мотивация

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

Если у вас есть функция, которая принимает несколько потенциально подвижных аргументов и будет перемещать их, если это возможно (например, factory function/constructor: Object create_object(string, vector<string>, string) или тому подобное), и вы хотите переместить или скопировать каждый аргумент, если нужно, вы быстро начнете писать много кода.

Если типы аргументов являются подвижными, тогда можно просто написать одну версию, которая принимает аргументы по значению, как указано выше. Но если аргументы являются (устаревшими) классами non-movable-but-swappable a la С++ 03, и вы не можете их изменить, то более эффективная запись ссылочных перегрузок rvalue более эффективна.

Итак, если lvalues ​​связывается с rvalues ​​через неявную копию, тогда вы можете написать только одну перегрузку, например create_object(legacy_string &&, legacy_vector<legacy_string> &&, legacy_string &&), и она будет более или менее работать как предоставление всех комбинаций ссылочных перегрузок rvalue/lvalue - фактические аргументы, которые были lvalues будут скопированы, а затем привязаны к аргументам, фактические аргументы, которые были rvalues, будут напрямую связаны.

Разъяснение/редактирование: Я понимаю, что это практически идентично принятию аргументов по значению для подвижных типов, таких как С++ 0x std::string и std::vector (за исключением количества перемещений конструктор концептуально вызывается). Однако он не идентичен для копируемых, но недвижущихся типов, который включает в себя все классы С++ 03 с явно определенными конструкторами копирования. Рассмотрим этот пример:

class legacy_string { legacy_string(const legacy_string &); }; //defined in a header somewhere; not modifiable.

void f(legacy_string s1, legacy_string s2); //A *new* (C++0x) function that wants to move from its arguments where possible, and avoid copying
void g() //A C++0x function as well
{
    legacy_string x(/*initialization*/);
    legacy_string y(/*initialization*/);

    f(std::move(x), std::move(y));
}

Если g вызывает f, тогда x и y будут скопированы - я не вижу, как компилятор может их перемещать. Если бы f были объявлены как принимающие аргументы legacy_string &&, это могло бы избежать тех копий, где вызывающий явным образом вызывал std::move аргументы. Я не вижу, как они эквивалентны.

Вопросы

Мои вопросы:

  • Является ли это допустимой интерпретацией стандарта? Похоже, что это не обычный или предполагаемый, во всяком случае.
  • Есть ли у него интуитивный смысл?
  • Есть ли проблема с этой идеей, которую я не вижу? Кажется, что вы могли бы получать копии, создаваемые тихо, когда это не совсем ожидалось, но статус-кво в местах на С++ 03 в любом случае. сделало бы некоторые перегрузки жизнеспособными, когда они в настоящее время нет, но я не вижу, что это проблема на практике.
  • Является ли это достаточно значительным улучшением, которое стоило бы сделать, например, экспериментальный патч для GCC?
4b9b3361

Ответ 1

Я не совсем понимаю вашу точку зрения в этом вопросе. Если у вас есть класс, который является подвижным, вам просто нужна версия T:

struct A {
  T t;
  A(T t):t(move(t)) { }
};

И если класс является традиционным, но имеет эффективный swap, вы можете записать версию подкачки или можете вернуться к const T& способу

struct A {
  T t;
  A(T t) { swap(this->t, t); }
};

Что касается версии подкачки, я предпочитаю использовать const T& путь вместо этого свопа. Основным преимуществом технологии свопинга является безопасность исключений и заключается в том, чтобы скопировать копию ближе к вызывающей стороне, чтобы она могла оптимизировать копии временных рядов. Но что вам нужно сохранить, если вы просто строите объект? И если конструктор мал, компилятор может заглянуть в него и также может оптимизировать копии.

struct A {
  T t;
  A(T const& t):t(t) { }
};

Мне кажется, что нет права автоматически преобразовывать строку lvalue в собственную копию rvalue только для привязки к ссылке rvalue. Ссылка rvalue говорит, что она связывается с rvalue. Но если вы попытаетесь привязать к lvalue того же типа, это лучше не удастся. Представляем скрытые копии, чтобы это не звучало правильно для меня, потому что, когда люди видят X&& и вы передаете значение X lvalue, я полагаю, что большинство будет ожидать, что нет копии, и это привязка напрямую, если это работает вообще. Лучше не сработайте сразу, чтобы пользователь мог исправить свой код.

Ответ 2

Как насчет этого кода?

void f(std::string &&); //NB: No const string & overload supplied

void g2(const std::string & arg)
{
    f(arg);
}

... Однако GCC и MSVC отклоняют g2 из-за положения 13.3.3.1.4/3, что запрещает lvalues ​​от привязки к аргументам refvalue. Я понимаю обоснование этого - объясняется в N2831 "Устранение проблемы безопасности с помощью ссылок rvalue". Я также думаю, что GCC, вероятно, реализует этот пункт, как это было предусмотрено авторами этой статьи, потому что исходный патч для GCC был написан одним из авторов (Doug Gregor)....

Нет, это только половина причины, по которой оба компилятора отклоняют ваш код. Другая причина заключается в том, что вы не можете инициализировать ссылку на не-const с выражением, ссылающимся на объект const. Так, еще до N2831 это не сработало. Просто нет необходимости в преобразовании, потому что строка - это уже строка. Кажется, вы хотите использовать string&& как string. Затем просто напишите свою функцию f так, чтобы она взяла строку по значению. Если вы хотите, чтобы компилятор создавал временную копию константной строки lvalue, чтобы вы могли вызывать функцию с помощью string&&, не было бы разницы между взятием строки по значению или rref, не так ли?

N2831 имеет мало общего с этим сценарием.

Если у вас есть функция, которая принимает несколько потенциально подвижных аргументов и будет перемещать их, если это возможно (например, функция/конструктор factory: Object create_object (строка, вектор, строка) или тому подобное) и хотите переместить или скопировать каждый аргумент, если необходимо, вы быстро начнете писать много кода.

Не совсем. Почему вы хотите написать много кода? Существует мало оснований загромождать весь ваш код при перегрузках const&/&&. Вы по-прежнему можете использовать одну функцию с сочетанием pass-by-value и pass-by-ref-to-const - в зависимости от того, что вы хотите делать с параметрами. Что касается фабрик, то идея заключается в использовании идеальной пересылки:

template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args)
{
    T* ptr = new T(std::forward<Args>(args)...);
    return unique_ptr<T>(ptr);
}

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

string foo();

int main() {
   auto ups = make_unique<string>(foo());
}

строка, возвращаемая foo, автоматически перемещается в кучу.

Итак, если lvalues ​​связывается с rvalues ​​через неявную копию, тогда вы можете написать только одну перегрузку, например create_object (legacy_string & &, legacy_vector &, legacy_string & &), и она будет более или менее работать подобно предоставлению всех комбинаций опорных перегрузок rvalue/lvalue...

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

Это достаточно значительное улучшение, которое стоило бы сделать, например. экспериментальный патч для GCC?

Нет улучшения.