Sol W3

Expected Observations for the Tracer Assignment

Output of Tracer

The observed output should look as follows:

--- Tracer m { "main" }; ---
Tracer created: main //(1)
--- Tracer inner { "inner" }; ---
Tracer created: inner //(2)
--- foo(inner); ---
Tracer copied: inner copy //(3)
Tracer created: foo //(4)
Tracer: inner copy //(5)
Tracer destroyed: foo //(6)
Tracer destroyed: inner copy //(7)
--- auto trace = bar(inner); ---
Tracer created: bar //(8)
Tracer: inner //(9)
---; ---
Tracer: bar //(10)
---; ---
Tracer: inner //(11)
--- } of compound statement ---
Tracer destroyed: bar //(12)
Tracer destroyed: inner //(13)
--- foo(Tracer{"temp"}); ---
Tracer created: temp //(14)
Tracer created: foo //(15)
Tracer: temp //(16)
Tracer destroyed: foo //(17)
Tracer destroyed: temp //(18)
---; ---
Tracer: main //(19)
--- } of main() ---
Tracer destroyed: main (20)


  1. Tracer with argument "main" gets created. It will live until the end of its block.
  2. Tracer with argument "inner" gets created. It will live until the end of its block.
  3. A copy of the inner tracer is created for the Tracer t parameter of foo.
  4. Tracer with argument "foo" gets created. It will live until the end of its block (the function foo).
  5. Show for the inner copy Tracer is called.
  6. End of foo the local objects get destroyed in reverse order. First the foo tracer.
  7. Then the inner copy tracer
  8. The bar call by itself does not generate any output. Therefore, the next out put is the creation of the Tracer with argument "bar". This construction can happen at the location of the return value and directly initialze the trace variable in main. This is a combination of named return value optimization and mandatory elision of C++17. Subsequently, no destructor will be called at the end of bar.
  9. Show for the original inner Tracer is called, from bar().
  10. Show for the bar Tracer (created in bar()) is called, by the call =[].
  11. Show for the original inner Tracer is called, from main().
  12. The local block ends at }, which ends the lifetime of the trace variable, which contains the bar Tracer.
  13. The end of the block also ends the lifetime of the inner Tracer.
  14. The Tracer{"temp"} prvalue is materialized when initializing the value parameter for void foo(Tracer t).
  15. Another foo Tracer is created, like above
  16. Show for the temp materialized Tracer is called.
  17. Destruction of the foo Tracer
  18. Destruction of the temp Tracer
  19. Show for the original main Tracer
  20. Destuction of the original main Tracer, at end of main().

Tracer in Standard Container

Observing the push_back Operations

At a glance, when checking the output of the four push_back operations there are more copies than expected. Especially, T1 is copied several (three in my case) times. These copies happen when adding elements to a vector that has reached the capacity of the array used to store the elements. When this happens a new (bigger) array is allocated and all elements are copied to this new array. The old array is then deallocated.

Below is an excerpt from the output to show this behavior. It is generated by the v.push_back(Tracer{"T2"}) statement.

Tracer created: T2           (1)
Tracer copied: T2 copy       (2)
Tracer copied: T1 copy copy  (3)
Tracer destroyed: T1 copy    (4)
Tracer destroyed: T2         (5)

Copying the vector

The result of copying the vector is straight-forward. Every element of the vector is copied again.

Tracer copied: T1 copy copy copy copy
Tracer copied: T2 copy copy copy
Tracer copied: T3 copy copy
Tracer copied: T4 copy copy


The implementation of the copy-assignment operator is as follows:

Tracer & operator=(Tracer const & rhs) {
  name = + " copy-assigned";
  std::cout << "Tracer copy-assigned: " << name << std::endl;
  return *this;

Note: The existence of the implicit copy-assignment operator when there is a user-defined destructor or copy-constructor is flawed in the standard. As you can see on Howard Hinnant's overview in the lecture.

Move Operations

The copy-constructor and the copy-assignment operator are replaced by the following implementations of the move-constructor and the move-assignment operator:

Tracer(Tracer && other) : name { + " moved" } { += " moved away";
  std::cout << "Tracer moved: " << name << std::endl;
Tracer & operator=(Tracer && other) {
  name += " move-assigned";
  std::cout << "Tracer move-assigned: " << name << std::endl; += " moved to";
  return *this;

#include <utility>
std::vector<Tracer> v_copy { std::move(v) };
sink = std::move(source);

Important: The use of inner after it has been moved from, is not allowed in general, until it gets assigned another Tracer. This is because its state is unknown. However, it must still be in destructible state.

Vector Again

When adding Tracers to the vector the copy operations are just replaced by the corresponding move operations. But, there is a change when moving the vector to another vector! This operation does not have any effect on the contained Tracer objects at all! Because they are left in the same memory location no additional move operation happens.

Moving vs. Copying Large Objects

It takes some elements to observe a timing difference between copy and move operations. But even on fast computers when copying or moving 1GB of data the difference should be measurable. If you cannot see the difference at any size, check whether you have enabled some kind of optimization in your compiler that eliminates the operations you are trying to measure completely.

Last edited March 8, 2018