преместете 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::преместя shared_ptr в конструктор на движение?   -  person eerorika    schedule 08.09.2014
comment
@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