Принятие параметров приемника по rvalue-ссылке, а не по значению, для обеспечения эффективного использования интерфейсов - PullRequest
1 голос
/ 04 мая 2020

В обзоре кода мы с коллегой обсуждали интерфейс для функции, которую я писал. Наша база кода использует C ++ 17, и мы не используем исключения (видеоигры).

Я утверждал, что способ получения параметров приемника idiomati c C ++ будет производительным при сохранении гибкости интерфейса, позволяя вызывающая сторона для передачи копии или перехода от собственного значения по желанию. Под идиоматическим способом c я подразумеваю либо одну функцию, принимающую параметр по значению, либо набор overoad для ссылок const lvalue и rvalue (что требует одного меньшего перемещения в случае lvalue за счет некоторого дублирования кода).

struct A {};

class ByValue
{
public:
    ByValue(std::vector<A> v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

class RefOverloads
{
public:
    RefOverloads(std::vector<A> const& v)
        : m_v(v)
    {}

    RefOverloads(std::vector<A>&& v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

int main()
{
    std::vector<A> v0;
    ByValue value0(v0);
    ByValue value1(std::move(v0));

    std::vector<A> v1;
    RefOverloads ref0(v1);
    RefOverloads ref1(std::move(v1));
}

С другой стороны, моему коллеге не нравится, что легко неявно делать дорогие копии. Он предпочел бы, чтобы эти аргументы приемника всегда были по ссылке rvalue (без перегрузки const lvalue ref), и если вызывающий объект хочет передать копию, он должен сделать локальную копию и переместить ее в функцию.

class RvalueRefOnly
{
public:
    RvalueRefOnly(std::vector<A>&& v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

int main()
{
    std::vector<A> v;
    //RvalueRefOnly failedCopy(v); // Fails purposefully.

    std::vector<A> vCopy = v;               // Explicit copy of v.
    RvalueRefOnly okCopy(std::move(vCopy)); // Move into okCopy.
}

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

void f(T x);

вызывающий знает, что f стал владельцем x. С

void g(T&& x);

g может иметь право собственности или нет, в зависимости от реализации f.

Есть ли лучший способ? Я пропускаю какой-то аргумент, так или иначе?

1 Ответ

0 голосов
/ 04 мая 2020

В основном у вас есть эти опции конструктора:

class myclass {
public:
    // #1 myclass(const std::string& s) : s(s) {}
    // #2 myclass(std::string&& s) : s(std::move(s)) {}

    // #3 myclass(std::string s) : s(std::move(s)) {}

    // #4 template <typename T> myclass(T&& t) : s(std::forward<T>(t)) {}

    std::string s;
};

#3 не может присутствовать с #1 или #2 -> неоднозначным вызовом

и вызовом

std::string s;
myclass A(s);
myclass B(std::string(s));
myclass C(std::move(s));
myclass D("temporary");
myclass E({5, '*'});

Ниже приведено количество конструкторов копирования / перемещения.

                        | A | B | C | D | E |
------------------------|---|---|---|---|---|
1                  Copy |<1>| 2 | 1 | 1 | 1 |
const l-value ref  Move |<0>| 1 | 1 | 0 | 0 |
                  Other |<0>| 0 | 0 | 1 | 1 |
------------------------|---|-v-|-v-|-v-|-v-|
2                  Copy | X |<1>|<0>| 0 |<0>|
r-value ref        Move | X |<1>|<1>| 1 |<1>|
                  Other | X |<0>|<0>| 1 |<1>|
------------------------|---|---|---|---|---|
3                  Copy | 1 | 1 | 0 | 0 |<0>|
by value           Move | 1 | 2 | 2 | 1 |<1>|
                  Other | 0 | 0 | 0 | 1 |<1>|
------------------------|---|---|---|---|---|
4                  Copy |<1>|<1>|<0>|<0>| X |
Forwarding ref     Move |<0>|<1>|<1>|<0>| X |
                  Other |<0>|<0>|<0>|<1>| X |
--------------------------------------------/

Возможные конфигурации:

  • #1 только: обрабатывать все случаи, но копировать для временных
  • #1/#2: (B / C / D / E будет использовать #2), поэтому наилучшие результаты за исключением конструкции на месте
  • #3 только: обрабатывать все случаи, но делает дополнительные move
  • #4 только: обработка большинства обычных случаев, лучшие результаты
  • #1/#2/#4: лучшие результаты (обратите внимание, что #4) имеет точное совпадение с неконстантным l-значением)
  • #2/#4: лучшие результаты
  • #2 только: запрещенная копия, но явная копия (B) делает на 1 ход больше, чем #1 / #2

Как вы можете видеть:

  • Ссылка на пересылку (#4) имеет лучшие результаты.
  • По const ref (#1) имеет лучшую производительность для копирования, но хуже производительность для других.
  • Тогда по значению (#3) является вторым «худшим», но только один дополнительный ход из лучших.

Другие очки:

  • только #1 один был доступен до C ++ 11 (так было по умолчанию в большинстве интерфейсов)
  • только #1 может означать отсутствие передачи прав собственности.
  • Только перемещение (#2) запрещает неявное копирование
  • Цель по значению #3 - написать только одну перегрузку как хороший компромисс.

Теперь сравните #1 / #2, #2 и #3:

  • Только для перемещения:

    • #1 / #2 не имеет значения
    • #3 немедленно опускается.
    • #2 имеет возможность обрабатывать гарантию исключения: если он выбрасывает, объект не потребляется.

    Если вы не хотите гарантированную и / или немедленную утечку, я бы использовал обход rvalue (#2).

    Для существующей кодовой базы я бы сохранял последовательность.

Для копируемых типов: - #1 / #2 - наиболее эффективный, но позволяет Копия - #3 удобен (и только один дополнительный ход), но допускает нежелательное копирование, гарантированный прием. - #2 избегайте нежелательных копий.

Теперь это в основном то, что вы хотите гарантировать и разрешить:

  • лучшая производительность -> #1/#2
  • нет ( неявные) копии -> #2
  • немедленный / гарантированный приемник -> #3
  • согласованность только с подвижными типами

    Для существующей кодовой базы я бы сохранять последовательность.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...