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

StackOverflow https://stackoverflow.com/questions/4321305

Вопрос

Мне интересно лучшую форму для моих конструкторов. Вот какой -то пример кода:

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 ход)

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

  1. По обоим пунктам, проход через константу не хуже, а иногда и лучше, чем пройти по значению. Кажется, это идет против дискуссии на «Хотите скорость? Пройти по значению».. Анкет Для C ++ (не C ++ 0x), должен ли я придерживаться проходов через константу для этих конструкторов или я должен пройти по значению? А для C ++ 0x я должен пройти через ссылку на проход по проходу по значению?

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

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

Это было полезно?

Решение

Я собрал несколько примеров. Я использовал 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 далеко.Это согласуется с C ++ FAQ. Анкет По сути, 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>

Итак, мы смогли заменить четыре копии с два движения Используя проход через 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 не строится. Таким образом, это медленнее, чем раньше, где не было необходимости копирования. Компиляторы C ++ 0x и операторы перемещения будут сокращать накладные расходы, но они все еще медленнее, чем возвращение по цене.

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

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

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

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
}

Если вы хотите скопировать в местоположение кадра, не складывающее, пройдите с помощью ссылки CONST Pre C ++ 0x и дополнительно пройти справочный rValue Post C ++ 0x

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

Если вы хотите скопировать в местоположении кадры без стека и swap, подумайте о прохождении по значению (Pre C ++ 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;
      }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top