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 касательно аргументов. Я понимаю объяснение позади этого - оно объяснено в N2831 "Фиксацию проблем безопасности с rvalue ссылками". Я также думаю, что GCC, вероятно, реализует этот пункт, как предназначено авторами той статьи, потому что исходный патч к GCC был записан одним из авторов (Doug Gregor).

Однако я не делаю это довольно интуитивно. Мне, (a) a const string & концептуально ближе к a 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), который посещает базовый урок значением.)

Мотивация

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

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

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

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

Разъяснение/редактирование: Я понимаю, что это фактически идентично принятию аргументов значением для подвижных типов, как C++ 0x станд.:: строка и станд.:: вектор (сохраняют для количества раз конструктора перемещения, концептуально вызывается). Однако это не идентично для copyable, но неподвижных типов, который включает весь 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. Существует ли проблема с этой идеей что я "m, не видя? Кажется, что Вы могли получить копии, бесшумно создаваемые, когда это точно не ожидается, но это - статус-кво в местах в C++ 03 так или иначе. Кроме того, это сделало бы некоторые перегрузки жизнеспособными, когда они в настоящее время не, но я не вижу, что он проблемой на практике.
  4. Действительно ли это - достаточно значительное улучшение, которое стоило бы сделать, например, экспериментальный патч для GCC?

16
задан Community 23 May 2017 в 11:48
поделиться

2 ответа

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

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

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

3
ответ дан 30 November 2019 в 23:24
поделиться

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

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

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

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

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

Не совсем.Зачем вам писать много кода? Нет особых причин загромождать весь ваш код перегрузками 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 ​​действительно привязывались к rvalue через неявную копию, тогда вы могли бы написать только одну перегрузку, например create_object (legacy_string &&, legacy_vector &&, legacy_string &&), и это более или менее работало бы как предоставление всех комбинаций rvalue / lvalue reference overloads ...

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

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

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

4
ответ дан 30 November 2019 в 23:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: