C ++ 0x rvalue ссылки - привязка lvalues-rvalue - PullRequest
16 голосов
/ 01 мая 2010

Это дополнительный вопрос к C ++ 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, используемом в IntelliSense MSVC).

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

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 ​​к аргументам rvalue ref. Я понимаю причину этого - это объясняется в N2831 «Устранение проблемы безопасности с помощью ссылок на значения». Я также думаю, что GCC, вероятно, реализует этот пункт, как задумано авторами этого документа, потому что оригинальный патч для GCC был написан одним из авторов (Даг Грегор).

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

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

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

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

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

Мотивация

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

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

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

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

Пояснение / редактирование: Я понимаю, что это практически идентично принятию аргументов по значению для подвижных типов, таких как C ++ 0x std :: string и std :: vector (за исключением числа раз конструктор перемещения вызывается концептуально). Однако он не идентичен для копируемых, но неподвижных типов, который включает все классы C ++ 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 для аргументов. Я не вижу, как они эквивалентны.

Вопросы

Мои вопросы:

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

Ответы [ 2 ]

4 голосов
/ 01 мая 2010

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

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 ​​связываться с аргументами rvalue ref. Я понимаю причину этого - это объясняется в N2831 «Устранение проблемы безопасности с помощью ссылок на значения». Я также думаю, что GCC, вероятно, реализует этот пункт, как задумано авторами этого документа, потому что оригинальный патч для GCC был написан одним из авторов (Даг Грегор) ....

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

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

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

Не совсем. Почему вы хотите написать много кода? Нет особых причин загромождать весь ваш код перегрузками const& / &&. Вы по-прежнему можете использовать одну функцию с комбинацией передачи по значению и передачи по ссылке в 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?

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

3 голосов
/ 01 мая 2010

Я не совсем понимаю вашу точку зрения в этом вопросе. Если у вас есть подвижный класс, то вам нужна версия 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, я уверен, что большинство из них будет ожидать, что копии не будет, и эта привязка будет прямой, если она работает на всех. Лучше сразу потерпеть неудачу, чтобы пользователь мог исправить свой код.

...