c++ - A canonical way to implement move semantics in a class -
i looking example of how implement derived , base class move semantics. more have thought it, more seems default move constructor , assignment move operator job because standard (stl) types , smart pointers default moveable.
anyway, in case have class hierarchy, requires explicit move implementation, how should - @ least first cut?
in example, using raw pointer wrap in std::unique_ptr needed example of move not default moveable.
any appreciated. :)
currently, have made following attempt:
struct bloba { char data[0xaa]; }; struct blobb { char data[0xbb]; }; //---------------------------------------- class base { public: //default construct base class //c++11 allows data members initialised, declared. otherwise here. base() { } //define destructor virtual ensure derived destructor gets called. (in case necessary. it's not in example still practice.) virtual ~base() { delete m_moveabledatainbase; //this contrived example show non-default moveable data, in practice raii should used rather deletes } //copy constructor, needs accept const ref base class copies data members base(const base& other) : m_moveabledatainbase(new bloba(*other.m_moveabledatainbase)), // copy other's moveable data m_pnonmoveabledatainbase(other.m_pnonmoveabledatainbase) // copy other's non-moveable data { } //assignment operator uses canonical copy swap idiom. returns reference allow chaining: = b = c //this implemented in terms of copy constructor. base& operator=(const base& rhs) { base temp(rhs); swap(temp); return *this; } //the move construtor declared throwing no exceptions called stl algorithms //it accepts rvalue , moves base part. base(base&& other) noexcept : m_moveabledatainbase(nullptr) // don't bother allocating our own resources moveable data because move (steal) other's resource { swap(other); } //the move assignment operator declared throwing no exceptions called stl algorithms //it accepts rvalue , moves (steals) data resources rhs using swap , copies non moveable data rhs base& operator=(base&& rhs) noexcept { //move (steal) moveable contents rhs std::swap(m_moveabledatainbase, rhs.m_moveabledatainbase); //copy non-moveable contents rhs m_pnonmoveabledatainbase = rhs.m_pnonmoveabledatainbase; return *this; } private: //this private member swaps data members' contents. //it private because isn't virtual , swaps base contents , not safe public interface void swap(base& other) { std::swap(m_moveabledatainbase, other.m_moveabledatainbase); std::swap(m_pnonmoveabledatainbase, other.m_pnonmoveabledatainbase); } //an example of large blob of data move instead of copy performance reasons. //normally, have used unique_ptr default moveable , need example of isn't bloba* m_moveabledatainbase{ new bloba }; //an example of data can't or don't want move int m_pnonmoveabledatainbase = 123; }; //---------------------------------------- class derived : public base { public: //default construct derived class, called after base class constructor //c++11 allows data members initialised, declared. otherwise here. derived() { } //default virtual destructor, clean stuff can't done automatically through raii virtual ~derived() { delete m_pmoveabledatainderived; //this contrived example show non-default moveable data, in practice raii should used rather deletes } //copy constructor, needs accept const ref derived class //first copy constructs base , copies derived data members derived(const derived& other) : base(other), // forward base copy constructor m_pmoveabledatainderived(new blobb(*other.m_pmoveabledatainderived)), // copy other's moveable data m_pnonmoveabledatainderived(other.m_pnonmoveabledatainderived) // copy other's non-moveable data { } //assignment operator uses canonical copy swap idiom. returns reference allow chaining: = b = c //because uses derived copy constructor, in turn copy constructs base, don't forward base assignment operator. derived& operator=(const derived& rhs) { derived temp(rhs); swap(temp); return *this; } //the move construtor declared throwing no eceptions called stl algorithms //it accepts rvalue , first moves base part , derived part. //there no point in allocating resource before moving in example, m_pblobb set nullptr derived(derived&& other) noexcept : base(std::move(other)), // forward base move constructor m_pmoveabledatainderived(nullptr) // don't bother allocating our own resources moveable data because move (steal) other's resource { swap(other); } //the move assignment operator declared throwing no exceptions called stl algorithms //it accepts rvalue , first calls base assignment operator , moves data resources rhs using swap derived& operator=(derived&& rhs) noexcept { //forward base move operator= base::operator=(std::move(rhs)); //move (steal) moveable contents rhs std::swap(m_pmoveabledatainderived, rhs.m_pmoveabledatainderived); //copy non-moveable contents rhs m_pnonmoveabledatainderived = rhs.m_pnonmoveabledatainderived; } private: //this member swaps derived data members contents. //it private because doesn't swap base contents , not safe public interface void swap(derived& other) noexcept { std::swap(m_pmoveabledatainderived, other.m_pmoveabledatainderived); std::swap(m_pnonmoveabledatainderived, other.m_pnonmoveabledatainderived); } //an example of large blob of data move instead of copy performance reasons. //normally, have used unique_ptr default moveable , need example of isn't blobb* m_pmoveabledatainderived{ new blobb }; //an example of data can't or don't want move int m_pnonmoveabledatainderived = 456; };
you have start out knowing class invariants are. class invariant or relationship true among data members. , want make sure special members can operate value satisfies class invariants. special members should not have preconditions (except class invariants must true).
let's take example example case discuss. first lets concentrate on base
. put private data members front close special members. way can more see defaulted or implicitly declared special members do.
base
class base { //an example of large blob of data move // instead of copy performance reasons. //normally, have used unique_ptr default moveable // , need example of isn't bloba* m_moveabledatainbase{ new bloba }; //an example of data can't or don't want move int m_pnonmoveabledatainbase = 123;
so far good, there slight ambiguity here: can m_moveabledatainbase == nullptr
? there no 1 right or wrong answer. question author of base
must answer, , write code enforce.
also, outline member functions. if decide want inline them, outside declaration. otherwise class declaration becomes difficult read:
class base { bloba* m_moveabledatainbase{ new bloba }; int m_pnonmoveabledatainbase = 123; public: virtual ~base(); base() = default; base(const base& other); base& operator=(const base& rhs); base(base&& other) noexcept; base& operator=(base&& rhs) noexcept; };
the destructor telling special member. declared/defined first:
base::~base() { delete m_moveabledatainbase; }
this looks good. not yet answer question whether m_moveabledatainbase
can nullptr
. next, if exists, default constructor. prefer = default
definitions when practical.
now copy constructor:
base::base(const base& other) : m_moveabledatainbase(new bloba(*other.m_moveabledatainbase)) , m_pnonmoveabledatainbase(other.m_pnonmoveabledatainbase) { }
ok, says significant:
other.m_moveabledatainbase != nullptr // ever
i glanced ahead , looked @ move constructor, , leaves moved-from value m_moveabledatainbase == nullptr
. have problem:
either there bug in copy constructor, , should check case
other.m_moveabledatainbase == nullptr
, orthere bug in move constructor , should not leave moved-from state
m_moveabledatainbase == nullptr
.
neither solution correct one. base
author has make design decision. if choses 2, there no reasonable way implement move constructor such faster copy constructor. in case thing not write move constructor , let copy constructor job. i'll choose 1 there still move constructor talk about. corrected copy constructor:
base::base(const base& other) : m_moveabledatainbase(other.m_moveabledatainbase ? new bloba(*other.m_moveabledatainbase) : nullptr) , m_pnonmoveabledatainbase(other.m_pnonmoveabledatainbase) { }
also, since chose invariant, might not bad idea revisit default constructor , instead say:
bloba* m_moveabledatainbase = nullptr;
now have noexcept
default constructor.
next comes copy assignment operator. not fall trap of selecting copy/swap idiom default. idiom fine. poorly performing. and performance more important code reuse. consider alternative copy/swap:
base& base::operator=(const base& rhs) { if (this != &rhs) { if (m_moveabledatainbase == nullptr) { if (rhs.m_moveabledatainbase != nullptr) m_moveabledatainbase = new bloba(*rhs.m_moveabledatainbase); } else // m_moveabledatainbase != nullptr { if (rhs.m_moveabledatainbase != nullptr) *m_moveabledatainbase = *rhs.m_moveabledatainbase; else { delete m_moveabledatainbase; m_moveabledatainbase = nullptr; } } m_pnonmoveabledatainbase = rhs.m_pnonmoveabledatainbase; } return *this; }
if common values of base
have m_moveabledatainbase != nullptr
, rewrite faster copy/swap. in common case, copy/swap 1 new , 1 delete. version 0 news , 0 deletes. copies 170 bytes.
and if had chosen design invariant m_moveabledatainbase != nullptr
, copy assignment gets simpler:
base& base::operator=(const base& rhs) { *m_moveabledatainbase = *rhs.m_moveabledatainbase; m_pnonmoveabledatainbase = rhs.m_pnonmoveabledatainbase; return *this; }
minimizing calls heap not premature optimization. engineering. move semantics made out of. precisely why std::vector
, std::string
copy assignment not use copy/swap idiom. slow.
move constructor: code this:
base::base(base&& other) noexcept : m_moveabledatainbase(std::move(other.m_moveabledatainbase)) , m_pnonmoveabledatainbase(std::move(other.m_pnonmoveabledatainbase)) { other.m_moveabledatainbase = nullptr; }
this saves few loads , stores. didn't bother inspecting generated assembly. urge before choosing implementation on one. in noexcept
move constructor, count loads , stores.
as style-guide, move
members when know scalars , move has no impact. saves reader of code having ensure non-moved members scalars.
your move assignment looks fine me:
base& base::operator=(base&& rhs) noexcept { //move (steal) moveable contents rhs std::swap(m_moveabledatainbase, rhs.m_moveabledatainbase); //copy non-moveable contents rhs m_pnonmoveabledatainbase = rhs.m_pnonmoveabledatainbase; return *this; }
the 1 time when don't want when have non-memory resources on lhs need destructed immediately, opposed swapped rhs. example swapping memory.
derived
for derived
write i've shown base
, except first copying/moving base
show in code. example here move constructor:
derived::derived(derived&& other) noexcept : base(std::move(other)) , m_pmoveabledatainderived(std::move(other.m_pmoveabledatainderived)) , m_pnonmoveabledatainderived(std::move(other.m_pnonmoveabledatainderived)) { other.m_pmoveabledatainderived = nullptr; }
also tag ~dervied()
override
instead of virtual
. want compiler tell if you've accidentally somehow not overridden ~base()
~derived()
.
class derived : public base { blobb* m_pmoveabledatainderived = nullptr; int m_pnonmoveabledatainderived = 456; public: ~derived() override; derived() = default; derived(const derived& other); derived& operator=(const derived& rhs); derived(derived&& other) noexcept; derived& operator=(derived&& rhs) noexcept; };
test
also test 6 special members (whether have them or not) static_assert
, type-traits:
static_assert(std::is_nothrow_destructible<base>{}, ""); static_assert(std::is_nothrow_default_constructible<base>{}, ""); static_assert(std::is_copy_constructible<base>{}, ""); static_assert(std::is_copy_assignable<base>{}, ""); static_assert(std::is_nothrow_move_constructible<base>{}, ""); static_assert(std::is_nothrow_move_assignable<base>{}, ""); static_assert(std::is_nothrow_destructible<derived>{}, ""); static_assert(std::is_nothrow_default_constructible<derived>{}, ""); static_assert(std::is_copy_constructible<derived>{}, ""); static_assert(std::is_copy_assignable<derived>{}, ""); static_assert(std::is_nothrow_move_constructible<derived>{}, ""); static_assert(std::is_nothrow_move_assignable<derived>{}, "");
you can test these blob
types:
static_assert(std::is_trivially_destructible<bloba>{}, ""); static_assert(std::is_trivially_default_constructible<bloba>{}, ""); static_assert(std::is_trivially_copy_constructible<bloba>{}, ""); static_assert(std::is_trivially_copy_assignable<bloba>{}, ""); static_assert(std::is_trivially_move_constructible<bloba>{}, ""); static_assert(std::is_trivially_move_assignable<bloba>{}, ""); static_assert(std::is_trivially_destructible<blobb>{}, ""); static_assert(std::is_trivially_default_constructible<blobb>{}, ""); static_assert(std::is_trivially_copy_constructible<blobb>{}, ""); static_assert(std::is_trivially_copy_assignable<blobb>{}, ""); static_assert(std::is_trivially_move_constructible<blobb>{}, ""); static_assert(std::is_trivially_move_assignable<blobb>{}, "");
summary
in summary, give each of 6 special members loving care deserve, if result inhibit them, implicitly declare them, or explicitly default or delete them. complier-generated move members move each base, move each non-static data member. prefer recipe, default when can, , augment when necessary.
highlight class api moving member function definitions out of class declaration.
test. @ least test or not have 6 special members, , if have them, if noexcept
or trivial (or not).
use copy/swap caution. can performance killer.
Comments
Post a Comment