Search code examples
c++movetbb

Does idiomatic (e.g. TBB's thread_enumerable_specific') move assignment call destructor on original object


let's say I'm working with an idiomatic Cpp library (e.g. Intel's TBB) and have an in-place member in some class (e.g. TsCountersType _ts_counters;). Such member is automatically initialized by its default constructor (if it exists, otherwise compile error) unless my own KMeans's constructor initialize it explicitely by calling its constructor directly.

Then if I assign a new (created by calling constructor) value to the member field in a normal non-constructor method (e.g. init), what exactly is safe to assume?

  1. I know that in that case the right side of the assignment is rvalue and thus move assignment operator will be called.
  2. I think it is safe to assume that well behaving idiomatic Cpp code should call the original objects's (e.g. _ts_counters) destructor before moving the new object to the memory location. But is such assumption sound?
  3. What about default move assignment operator? Does it call destructor on the original object before moving in? Is that even relevant question or is default move assignment operator created by the compiler only if (among other conditions) no explicit destructor is defined?

    1. If that's the case what happens if I have a scenario similar to the TBB one where instead of TsCountersType I have simple custom tipe with a single unique_ptr and default everything else (constructors, destructors, move assignments, …). When will the unique_ptr get out of scope then?
  4. Specifically in TBB's case, can I assume it happens given their documentation: Supported since C++11. Moves the content of other to *this intact. other is left in an unspecified state, but can be safely destroyed.

Example code:

class KMeans 
{
  private:
    //...
    // thread specific counters
    typedef std::pair<std::vector<point_t>, std::vector<std::size_t>> TSCounterType;
    typedef tbb::enumerable_thread_specific<TSCounterType> TsCountersType;
    TsCountersType _ts_counters;

  public:
    //...
    KMeans() : _tbbInit(tbb::task_scheduler_init::automatic) {}

    virtual void init(std::size_t points, std::size_t k, std::size_t iters)
    {
        // When _ts_counters is replaced by a new object the destructor is automatically called on the original object
        _ts_counters = TsCountersType(std::make_pair(std::vector<point_t>(k), std::vector<std::size_t>(k)));

    }
};

Solution

  • Consider this code:

    #include<iostream>
    
    struct S
    {
        S() { std::cout << __PRETTY_FUNCTION__ << std::endl;}
        S(S const &) { std::cout << __PRETTY_FUNCTION__ << std::endl;}
        S(S&&) { std::cout << __PRETTY_FUNCTION__ << std::endl;}
        S& operator=(S const &) { std::cout << __PRETTY_FUNCTION__ << std::endl; return *this;}
        S& operator=(S&&) { std::cout << __PRETTY_FUNCTION__ << std::endl; return *this;}
        ~S() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    };
    
    struct W
    {
        W() : s{} {}
        void init() { s = S{}; }
    private:
        S s;
    };
    
    int main()
    {
        W w{};
        w.init();
    }
    

    The output this produces (clang and gcc tested):

    S::S()
    S::S()
    S &S::operator=(S &&)
    S::~S()
    S::~S()
    

    So, what happens exactly:

    • W's constcructor is called, which default initializes S
    • A temporary S is default constructed and immediately moved from into W::s member
    • Temporary object is now destructed (just before init exits)
    • W::s is destructed on main exit.