C++0x rvalue ссылки - привязка lvalues-rvalue
Это дополнительный вопрос к ссылкам и временным ссылкам 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)
и т. п.), и, если необходимо переместить или скопировать каждый аргумент, вы быстро начнете писать много кода.
Если типы аргументов являются подвижными, то можно просто написать одну версию, которая принимает аргументы по значению, как указано выше. Но если аргументы - это (устаревшие) классы, не являющиеся перемещаемыми, но заменяемыми, такие как C++03, и вы не можете их изменить, тогда запись перегруженных ссылок на rvalue более эффективна.
Так что, если lvalues связывается с rvalues через неявную копию, вы можете написать только одну перегрузку, как create_object(legacy_string &&, legacy_vector<legacy_string> &&, legacy_string &&)
и это будет более или менее работать, как предоставление всех комбинаций перегруженных ссылок rvalue / lvalue - фактические аргументы, которые были lvalues, будут скопированы и затем привязаны к аргументам, фактические аргументы, которые были rvalue, будут напрямую связаны.
Пояснение / редактирование: я понимаю, что это практически идентично принятию аргументов по значению для подвижных типов, таких как 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
на аргументы. Я не вижу, как они эквивалентны.
Вопросы
Мои вопросы тогда:
- Это действительная интерпретация стандарта? Похоже, что это не обычный или предполагаемый, во всяком случае.
- Имеет ли это интуитивный смысл?
- Есть ли проблема с этой идеей, которую я не вижу? Похоже, что вы можете спокойно создавать копии, когда это не совсем ожидаемо, но это статус-кво в местах в C++03 в любом случае. Кроме того, это может привести к некоторым Перегрузки жизнеспособны, когда их нет, но я не вижу в этом проблемы на практике.
- Является ли это достаточно значительным улучшением, так что стоило бы сделать, например, экспериментальный патч для GCC?
2 ответа
Я не совсем понимаю вашу точку зрения в этом вопросе. Если у вас есть подвижный класс, то вам просто нужно 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
Я уверен, что большинство из них будет ожидать, что копии нет, и эта привязка выполняется напрямую, если она вообще работает. Лучше сразу потерпеть неудачу, чтобы пользователь мог исправить свой код.
Как насчет этого кода?
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?
Там нет улучшения.