переместить shared_ptr в список инициализации конструктора

Недавно я видел несколько примеров кода, подобного этому, где std::move использовался в списке инициализации конструктора (а не в конструкторе перемещения).

class A {
public:
    A(std::shared_ptr<Res> res) : myRes(std::move(res)) {
    // ...
    }

private:
    std::shared_ptr<Res> myRes;
}

Я получил информацию, что эта конструкция была сделана из соображений оптимизации. Лично я использую std::move как можно реже. Я угрожаю им как броску (как сказал Скотт Мейерс) и только в коде вызывающей стороны (единственное исключение - конструктор перемещения). Для меня это выглядит как какая-то обфускация или микрооптимизация, но возможно я ошибаюсь. Правда ли, что без std::move компилятор не выдает более быстрый код?


person NiegodziwyBeru    schedule 07.09.2014    source источник
comment
Я думаю, что цель состоит в том, чтобы избежать атомарного приращения счетчика(ов) ссылок. Это также вопрос точного выражения вашего намерения и довольно распространенного шаблона (передача по значению, затем перемещение локального параметра).   -  person dyp    schedule 08.09.2014
comment
возможный дубликат Должен ли я std::move a shared_ptr в конструктор перемещения?   -  person eerorika    schedule 08.09.2014
comment
@user2079303 user2079303 Я не уверен, что это полный дубликат этого конкретного вопроса. Основное внимание уделяется перемещению члена данных в конструкторе перемещения; это отличается от проблемы в этом вопросе. Тем не менее, часть производительности частично решена.   -  person dyp    schedule 08.09.2014
comment
Справедливо, я отозвал голосование, но оставлю ссылку, так как считаю, что она имеет отношение к аспекту производительности.   -  person eerorika    schedule 08.09.2014
comment
Связано: Почему мы копируем, а затем перемещаем?   -  person 0x499602D2    schedule 08.09.2014
comment
Мне кажется, что вы действительно должны передавать unique_ptr, а не shared_ptr в первую очередь. Вы не делитесь указателем здесь, вы даете его.   -  person screwnut    schedule 08.09.2014


Ответы (1)


Я считаю отсутствие std::move(), когда нетривиальный объект может быть перемещен, но компиляторы не могут определить, что это так, является ошибкой в ​​коде. То есть std::move() в конструкторе является обязательным: ясно, что временный объект, с которым вызывается конструктор, вот-вот выйдет за пределы области видимости, т. е. его можно безопасно переместить. С другой стороны, построение переменных-членов из аргумента не является одним из случаев, когда можно исключить копию. То есть компилятор должен создать копию, что, конечно, не очень дорого для std::shared_ptr<T>, но и не бесплатно. В частности, обновленные счетчики ссылок должны быть синхронизированы. Другой вопрос, можно ли измерить разницу. Запуск простого теста (см. ниже), кажется, означает, что улучшение производительности действительно есть. Обычно результаты, которые я получаю, такие:

// clang:
copy: 440
move: 206
copy: 414
move: 209
// gcc:
copy: 361
move: 167
copy: 335
move: 170

Обратите внимание, что в этом контексте вы являетесь вызываемым конструктором члена! Правильно, что std::move(res) — это просто причудливый способ написать приведение (это замена static_cast<std::shared_ptr<RES>&&>(res)). Однако крайне важно использовать его в местах, где объекты вот-вот выйдут за рамки, но в противном случае будут скопированы. С семантической точки зрения использование std::move() во многих случаях не имеет значения (оно имеет смысл только при работе с подвижными, но не копируемыми типами). Избегание ненужных копий является важным улучшением производительности, и std::move() помогает сделать это в контексте, когда компиляторы не могут сделать вывод, что это нормально или что это не разрешено: конкретный случай - это то, что компилятор, вероятно, мог бы даже обнаружить самостоятельно. что ход будет безопасным, но нельзя заменить копию ходом. Было бы неплохо, если бы компиляторы предупреждали об отсутствии std::move() в таких случаях!

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <memory>
#include <ostream>
#include <vector>

class timer
{
    typedef std::chrono::high_resolution_clock clock;
    clock::time_point d_start;
public:
    timer(): d_start(clock::now()) {}
    std::ostream& print(std::ostream& out) const {
        using namespace std::chrono;
        return out << duration_cast<microseconds>(clock::now() - this->d_start).count();
    }
};

std::ostream& operator<< (std::ostream& out, timer const& t)
{
    return t.print(out);
}

struct ResCopy
{
    std::shared_ptr<unsigned int> d_sp;
    ResCopy(std::shared_ptr<unsigned int> sp): d_sp(sp) {}
    unsigned int value() const { return *this->d_sp; }
};

struct ResMove
{
    std::shared_ptr<unsigned int> d_sp;
    ResMove(std::shared_ptr<unsigned int> sp): d_sp(std::move(sp)) {}
    unsigned int value() const { return *this->d_sp; }
};

template <typename Res>
void measure(char const* name, std::vector<std::shared_ptr<unsigned int>> const& v)
{
    timer t;
    unsigned long value(0);
    for (int c(0); c != 100; ++c) {
        for (std::size_t i(0), end(v.size()); i != end; ++i) { 
            value += Res(v[i]).value();
        }
    }
    std::cout << name << ": " << t << '\n';
}

int main()
{
    std::vector<std::shared_ptr<unsigned int>> v;
    std::generate_n(std::back_inserter(v), 100,
                    []{ return std::shared_ptr<unsigned int>(new unsigned int(std::rand())); });

    measure<ResCopy>("copy", v);
    measure<ResMove>("move", v);
    measure<ResCopy>("copy", v);
    measure<ResMove>("move", v);
}
person Dietmar Kühl    schedule 07.09.2014