Ссылки на Rvalue и дизайн классов

Я пытался оценить, как ссылки rvalue влияют на дизайн класса. Скажем, у меня есть существующий класс, как показано ниже.

class X
{
   string internal;

public:
   void set_data(const char* s)
   {
      internal = s;
   }
..
..
..
//other stuff

};

Этот класс используется другим модулем следующим образом:

//another module
{

    string configvalue;
    X x;

    //read configvalue from a file and call set 

    ...
    x.set_data(configvalue.c_str());

    //use x to do some magic
    ..
    ...


}

со ссылками на rvalue будет лучше предоставить другую функцию-член, например

class X
{
...
...
....
 void set_data(string s)
 {
     internal = std::move(s);
 }
};

Это позволит клиентам этого класса использовать семантику перемещения и предотвратить один набор операций выделения/копирования за одно использование. Это сильно придуманный пример, но один и тот же принцип применим ко всем проектам классов без нарушения парадигмы «минимального интерфейса».

Любое понимание этого вопроса высоко ценится?


person user1751716    schedule 17.10.2012    source источник


Ответы (2)


Да, добавление перегрузки string, как вы предлагаете, является хорошей идеей. Даже без ссылок на rvalue такая перегрузка была бы хорошей идеей. В противном случае, учитывая std::string s, для его использования пришлось бы:

x.set_data(s.c_str());

тогда как

x.set_data(s);

намного более интуитивно понятен (и даже немного эффективнее) для клиентов X.

В качестве другого варианта вы можете добавить эти две перегрузки:

void set_data(const string& s) {internal = s;}
void set_data(string&& s)      {internal = std::move(s);}

Это примерно эквивалентно единственной перегрузке, которую вы правильно предложили. Решение с двумя перегрузками дает очень небольшое преимущество в производительности. Решение с одинарной перегрузкой будет стоить дополнительной конструкции string перемещения, если переданный аргумент является значением x (значение l, которое было преобразовано с помощью std::move). Но конструктор перемещения std::string должен быть очень быстрым, так что это не должно иметь большого значения. Я упоминаю об этом только в духе полного раскрытия.

Если set_data имеет более одного параметра, подход "по значению" становится гораздо более привлекательным. Например, рассмотрим случай, когда вам нужно передать два string. Ваш выбор:

Решение 1

void set_data(string s1, string s2);

Решение 2

void set_data(const string&  s1, const string&  s2);
void set_data(      string&& s1, const string&  s2);
void set_data(const string&  s1,       string&& s2);
void set_data(      string&& s1,       string&& s2);

Как вы можете быстро заметить, Решение 2 плохо масштабируется с количеством параметров.

Наконец, ни при каких обстоятельствах вы не должны пытаться применять оба решения к одному и тому же типу:

Не делайте этого!

void set_data(string s)        {internal = std::move(s);}
void set_data(const string& s) {internal = s;}
void set_data(string&& s)      {internal = std::move(s);}

Этот набор перегрузок будет неоднозначным. Как и в C++03, следующие две перегрузки неоднозначны:

void set_data(string s)        {internal = std::move(s);}
void set_data(const string& s) {internal = s;}

Никогда не перегружайте значение по значению ссылкой, будь то ссылка lvalue или ссылка rvalue.

person Howard Hinnant    schedule 16.11.2012
comment
void set_data(const string& s) {internal = s;} void set_data(string&& s) {internal = std::move(s);}, когда позвонят в первый раз и когда во второй? - person baye; 17.11.2012
comment
Первая версия предпочтет привязку к lvalue... вторая версия предпочтет привязку к rvalue, а также к lvalue, обернутым в std::move. - person Jason; 17.11.2012
comment
Что касается передачи нескольких параметров - да, это плохо масштабируется, но это не должно быть причиной выбора подхода по значению. Это может быть легко решено (и, вероятно, должно быть из соображений дизайна) путем сохранения аргументов в одном объекте конфигурации, который будет передан по ссылке в перегруженную функцию, что сведет проблему к исходной с одним аргументом. - person SomeWittyUsername; 17.11.2012

Я не вижу причин, чтобы void set_data(const char* s) и void set_data(string s) были частью интерфейса. Это создаст двусмысленность и чревато побочными эффектами. Более того, вы по-прежнему передаете аргумент по значению при вызове set_data(string s). Вместо этого я бы предложил определить две следующие функции:

void set_data(const string &s);
void set_data(string &&s);

Таким образом, у вас может быть 2 реализации: первая будет глубоко копировать вашу строку, а вторая может украсть внутренности строки, поскольку это rvalue (не забудьте оставить ее в определенном состоянии, чтобы деструктор мог ее уничтожить без проблем - подробности см. на http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2027.html#Move_Semantics).

Вторая версия будет вызываться автоматически либо при rvalue string аргументе, либо если аргумент принудительно равен rvalue, например, std::move.

Если вы также хотите иметь параметр по значению, вы можете использовать версию rvalue этого API в сочетании с конструктором копирования строк: set_data(string(str)).

person SomeWittyUsername    schedule 16.11.2012
comment
@HowardHinnant Нет. А ты? - person SomeWittyUsername; 16.11.2012
comment
Да. Когда я реализую ваши первые два set_data и вызов с rvalue string, мой компилятор жалуется на двусмысленность. Когда я добавляю вашу третью перегрузку set_data, я также получаю двусмысленность при вызове с lvalue string. - person Howard Hinnant; 17.11.2012
comment
а, да - из-за стоимости. Его следует удалить, и при необходимости тот же результат может быть достигнут двумя другими и каким-либо методом клонирования, например - person SomeWittyUsername; 17.11.2012
comment
@HowardHinnant, да, вы правы - из-за значения. Его следует удалить, и при необходимости того же результата можно добиться, например, двумя другими и каким-либо методом клонирования. Я отредактировал ответ. - person SomeWittyUsername; 17.11.2012