Подтвердить что ты не робот

Лучшая форма для конструкторов? Передать по значению или ссылке?

Мне интересно, какая форма для моих конструкторов. Вот пример кода:

class Y { ... }

class X
{
public:
  X(const Y& y) : m_y(y) {} // (a)
  X(Y y) : m_y(y) {} // (b)
  X(Y&& y) : m_y(std::forward<Y>(y)) {} // (c)

  Y m_y;
}

Y f() { return ... }

int main()
{
  Y y = f();
  X x1(y); // (1)
  X x2(f()); // (2)
}

Из того, что я понимаю, это лучшее, что может сделать компилятор в каждой ситуации.

(1a) y копируется в x1.m_y (1 экземпляр)

(1b) y копируется в аргумент конструктора X, а затем копируется в x1.m_y (2 экземпляра)

(1c) y перемещается в x1.m_y (1 ход)

(2a) результат f() копируется в x2.m_y (1 экземпляр)

(2b) f() строится в аргументе конструктора, а затем копируется в x2.m_y (1 экземпляр)

(2c) f() создается в стеке, а затем перемещается в x2.m_y (1 ход)

Теперь несколько вопросов:

  • По обоим подсчетам, pass by const ссылается не хуже, а иногда лучше, чем по значению. Это, похоже, противоречит обсуждению "Want Speed? Pass by Value" .. Для С++ (а не С++ 0x) следует ли придерживаться ссылки pass by const для этих конструкторов, или мне нужно перейти по значению? И для С++ 0x, должен ли я передавать значение rvalue через pass по значению?

  • Для (2), я бы предпочел бы, чтобы временное было построено непосредственно в x.m_y. Даже версия rvalue, на мой взгляд, требует перемещения, которое, если объект не распределяет динамическую память, не работает как копия. Есть ли способ кодировать это, чтобы компилятору было разрешено избегать этих копий и перемещаться?

  • Я сделал много предположений в том, что, по моему мнению, компилятор может сделать лучше всего и в моих вопросах. Пожалуйста, исправьте их, если они неверны.

4b9b3361

Ответ 1

Я собрал несколько примеров. Я использовал GCC 4.4.4 во всем этом.

Простой случай, без -std=c++0x

Сначала я собрал очень простой пример с двумя классами, которые принимают std::string каждый.

#include <string>
#include <iostream>

struct A /* construct by reference */
  {
    std::string s_;

    A (std::string const &s) : s_ (s)
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    std::string s_;

    B (std::string s) : s_ (s)
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }

int main ()
  {
    A a (f ());
    A a2 (f2 ());
    B b (g ());
    B b2 (g2 ());

    return 0;
  }

Вывод этой программы на stdout выглядит следующим образом:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>

Заключение

GCC смог оптимизировать все временные A или B. Это согласуется с часто задаваемые вопросы по С++. В принципе, GCC может (и хочет) генерировать код, который создает a, a2, b, b2 на месте, даже если вызывается функция, которая явно возвращается по значению. Таким образом, GCC может избежать многих временных ситуаций, существование которых можно было бы "вывести", просмотрев код.

Следующее, что мы хотим увидеть, - это то, как часто std::string фактически копируется в приведенном выше примере. Позвольте заменить std::string тем, что мы можем наблюдать лучше и видеть.

Реалистичный случай, без -std=c++0x

#include <string>
#include <iostream>

struct S
  {
    std::string s_;

    S (std::string const &s) : s_ (s)
      {
        std::cout << "  S::<constructor>" << std::endl;
      }
    S (S const &s) : s_ (s.s_)
      {
        std::cout << "  S::<copy constructor>" << std::endl;
      }
    ~S ()
      {
        std::cout << "  S::<destructor>" << std::endl;
      }
  };

struct A /* construct by reference */
  {
    S s_;

    A (S const &s) : s_ (s) /* expecting one copy here */
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    S s_;

    B (S s) : s_ (s) /* expecting two copies here */
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }

/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }

/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }

/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }

int main ()
  {
    A a (f ());
    std::cout << "" << std::endl;
    A a2 (f2 ());
    std::cout << "" << std::endl;
    B b (g ());
    std::cout << "" << std::endl;
    B b2 (g2 ());
    std::cout << "" << std::endl;

    return 0;
  }

И результат, к сожалению, соответствует ожиданию:

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

Заключение

GCC не удалось оптимизировать временный S, созданный конструктором B. Использование конструктора копии по умолчанию S не изменило его. Изменение f, g на

static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!

имел указанный эффект. Похоже, что GCC готов построить аргумент для конструктора B на месте, но не решался построить член B на месте. Обратите внимание, что все еще не созданы временные A или B. Это означает, что a, a2, b, b2 все еще строится на месте. Круто.

Теперь рассмотрим, как новая семантика перемещения может повлиять на второй пример.

Реалистичный случай, при -std=c++0x

Рассмотрим добавление следующего конструктора к S

    S (S &&s) : s_ ()
      {
        std::swap (s_, s.s_);
        std::cout << "  S::<move constructor>" << std::endl;
      }

И сменив конструктор B на

    B (S &&s) : s_ (std::move (s)) /* how many copies?? */
      {
        std::cout << "B::<constructor>" << std::endl;
      }

Получаем этот вывод

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

Итак, мы смогли заменить четыре копии двумя ходами, используя pass by rvalue.

Но мы на самом деле создали сломанную программу.

Напомним g, g2

static B g ()  { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }

Отмеченное местоположение показывает проблему. Перемещение было выполнено на объект, который не является временным. Это потому, что ссылки rvalue ведут себя как ссылки lvalue, за исключением того, что они также могут привязываться к временным. Поэтому мы не должны забывать перегружать конструктор B с тем, который принимает постоянную ссылку lvalue.

    B (S const &s) : s_ (s)
      {
        std::cout << "B::<constructor2>" << std::endl;
      }

Затем вы заметите, что оба g, g2 вызывают "конструктор2", потому что символ S в любом случае лучше подходит для ссылки const, чем для ссылки rvalue. Мы можем убедить компилятор сделать ход в g одним из двух способов:

static B g ()  { return B (S ("string")); }
static B g ()  { S s ("string"); return B (std::move (s)); }

Заключение

Возвращаем значение. Код будет более читаемым, чем код "заполнить ссылку, я даю вам", и быстрее и, возможно, даже более безопасный вариант.

Рассмотрим изменение f на

static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }

Это встретит сильную гарантию, только если это присваивает A. Копировать в result нельзя пропустить, и tmp нельзя построить вместо result, так как result не создается. Таким образом, он медленнее, чем раньше, когда копирование не требуется. Компиляторы С++ 0x и операторы переадресации сокращают накладные расходы, но все еще медленнее, чем возврат к значению.

Возвращаемое значение обеспечивает более простую гарантию. Объект построен на месте. Если одна часть этого сбоя и другие части уже построены, нормальное разматывание будет очищено и, если конструктор S выполняет основную гарантию в отношении своих собственных членов и сильную гарантию в отношении глобальных элементов, процесс возврата по значению фактически обеспечивает надежную гарантию.

Всегда передавайте по значению, если вы собираетесь копировать (в стек) в любом случае

Как обсуждалось в Хотите скорость? Перейдите по значению.. Компилятор может генерировать код, который строит, если возможно, аргумент вызывающего абонента на месте, исключая копию, которую он не может сделать, когда вы берете по ссылке, а затем копируете вручную. Основной пример: Do НЕ пишите это (взято из цитируемой статьи)

T& T::operator=(T const& x) // x is a reference to the source
{ 
    T tmp(x);          // copy construction of tmp does the hard work
    swap(*this, tmp);  // trade our resources for tmp's
    return *this;      // our (old) resources get destroyed with tmp 
}

но всегда предпочитайте это

T& T::operator=(T x)    // x is a copy of the source; hard work already done
{
    swap(*this, x);  // trade our resources for x's
    return *this;    // our (old) resources get destroyed with x
}

Если вы хотите скопировать в папку с кадром нестационарной передачи по константной ссылке pre С++ 0x и дополнительно пройти по rvalue reference post С++ 0x

Мы уже это видели. Передача по ссылке приводит к уменьшению количества копий, когда строительство на месте невозможно, чем передавать по значению. И семантика перемещения С++ 0x может заменить многие копии с меньшими и более дешевыми ходами. Но имейте в виду, что перемещение сделает зомби вне объекта, из которого был перемещен. Перемещение не копируется. Просто предоставление конструктора, который принимает ссылки rvalue, может сломать вещи, как показано выше.

Если вы хотите скопировать в местоположение фрейма без стека и swap, рассмотрите возможность передачи по значению в любом случае (pre С++ 0x)

Если у вас дешевая конструкция по умолчанию, которая в сочетании с swap может быть более эффективной, чем копирование материала вокруг. Рассмотрим конструктор S как

    S (std::string s) : s_ (/* is this cheap for your std::string? */)
      {
        s_.swap (s); /* then this may be faster than copying */
        std::cout << "  S::<constructor>" << std::endl;
      }