I made an object tracker for debugging and testing purposes called ccm_counter
(construction, copy, move counter). It counts constructor, copy and move calls. It can be used to detect inefficient forwarding and unnecessary (or unintended) copies. The behavior is as follows:
What it does
Let vector<A>
be the type you want to test and A
its element type (A
must be move and copy constructable and must not be final). Drop in the counter like vector<ccm_counter<A>>
. ccm_counter
behaves like A
except that it also counts:
- constructor calls
- copy constructor calls
- move constructor calls
- copy assignment calls
- move assignment calls
- copies
- moves
Point 1 to 5 are bound to the object (or variable) while point 6 and 7 are bound to the value of the object. For example:
struct A{};
ccm_counter<A> a, b; // a and b increment constructor call count by 1
b = a; // a +1 copies -> import copies and moves from a -> b +1 copy assignment
a = b; // b +1 copies -> import copies and moves from b -> a +1 copy assignment
b = a;
This example results in (shortened)
a:
constructor calls: 1
copy assignment calls: 1
total copies made: 3
b:
constructor calls: 1
copy assignment calls: 2
total copies made: 3
Now we add the vector<A>
(the examples uses std::vector
):
struct A{};
vector<ccm_counter<A>> vec;
ccm_counter<A> a, b;
vec.shrink_to_fit();
vec.push_back(a);
vec.push_back(a);
vec.push_back(a);
vec.push_back(b);
for (int i = 0; i < vec.size(); i++)
cout << "element " << i << ":\n" << vec[i].get_object_stats() << "\n" << *vec[i].get_value_stats() << "\n\n";
which prints:
element 0:
constructor calls: 0
copy constructor calls: 0
move constructor calls: 1
copy assignment calls: 0
move assignment calls: 0
total copies made: 3
total moves made: 6
element 1:
constructor calls: 0
copy constructor calls: 0
move constructor calls: 1
copy assignment calls: 0
move assignment calls: 0
total copies made: 3
total moves made: 6
element 2:
constructor calls: 0
copy constructor calls: 0
move constructor calls: 1
copy assignment calls: 0
move assignment calls: 0
total copies made: 3
total moves made: 6
element 3:
constructor calls: 0
copy constructor calls: 1
move constructor calls: 0
copy assignment calls: 0
move assignment calls: 0
total copies made: 1
total moves made: 0
The constructor and assignment call counts are not very useful in this example, because they are bound to the object itself, which, in this case, is just an element in the internal buffer of the vector (which also changes as soon as the buffer is reallocated). However, the total copies and moves were tracked by the value. Element 0 gets copied when we call vec.push_back(a)
the first time. Element 1 gets copied when we call vec.push_back(a)
the second time. Because the vector exceeds his size each time we call vec.push_back(...)
, its internal buffer has to be reallocated each time. This results in element 0 beeing moved into the new buffer which is reflected in the output and so on.
Implementation
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <memory>
#include <ostream>
#include <type_traits>
struct ccm_object_stats
{
std::size_t ctor = 0; // number of (any, except copy and move) constructor calls
std::size_t copy_ctor = 0; // number of copy constructor calls
std::size_t copy_assign = 0; // number of copy assignment calls
std::size_t move_ctor = 0; // number of move constructor calls
std::size_t move_assign = 0; // number of move assignment calls
constexpr ccm_object_stats& operator+=(const ccm_object_stats& rhs)
{
ctor += rhs.ctor;
copy_ctor += rhs.copy_ctor;
move_ctor += rhs.move_ctor;
copy_assign += rhs.copy_assign;
move_assign += rhs.move_assign;
return *this;
}
constexpr ccm_object_stats operator+(const ccm_object_stats& rhs)
{
ccm_object_stats ret(*this);
return ret += rhs;
}
};
std::ostream& operator<<(std::ostream& os, const ccm_object_stats& stats)
{
using namespace std;
constexpr size_t sw = 24;
const size_t nw = static_cast<size_t>(
log10(max({ stats.ctor, stats.copy_ctor, stats.copy_assign, stats.move_ctor, stats.move_assign }))) + 1;
os
<< setw(sw) << left << "constructor calls: " << setw(nw) << right << stats.ctor << "\n"
<< setw(sw) << left << "copy constructor calls: " << setw(nw) << right << stats.copy_ctor << "\n"
<< setw(sw) << left << "move constructor calls: " << setw(nw) << right << stats.move_ctor << "\n"
<< setw(sw) << left << "copy assignment calls: " << setw(nw) << right << stats.copy_assign << "\n"
<< setw(sw) << left << "move assignment calls: " << setw(nw) << right << stats.move_assign;
return os;
}
struct ccm_value_stats
{
std::size_t copies = 0; // number of copies made
std::size_t moves = 0; // number of moves made
constexpr ccm_value_stats& operator+=(const ccm_value_stats& rhs)
{
copies += rhs.copies;
moves += rhs.moves;
return *this;
}
constexpr ccm_value_stats operator+(const ccm_value_stats& rhs)
{
ccm_value_stats ret(*this);
return ret += rhs;
}
};
std::ostream& operator<<(std::ostream& os, const ccm_value_stats& stats)
{
using namespace std;
constexpr size_t sw = 24;
const size_t nw = static_cast<size_t>(
log10(max({ stats.copies, stats.moves }))) + 1;
os
<< setw(sw) << left << "total copies made: " << setw(nw) << right << stats.copies << "\n"
<< setw(sw) << left << "total moves made: " << setw(nw) << right << stats.moves;
return os;
}
// A wrapper object that inherits from `T` and counts construction, copy and move operations of `T`.
template <typename T>
class ccm_counter : public T
{
public:
template <typename ...Args, typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
constexpr explicit ccm_counter(Args... args) noexcept(std::is_nothrow_constructible_v<T, Args...>)
: T(std::forward<Args>(args)...), _val_stats{ std::make_shared<ccm_value_stats>() }
{
_obj_stats.ctor++;
}
constexpr ccm_counter(const ccm_counter& other) noexcept(std::is_nothrow_copy_constructible_v<T>)
: T(other), _val_stats(other._val_stats)
{
static_assert(std::is_copy_constructible_v<T>, "T must be copy constructible.");
_val_stats->copies++;
_obj_stats.copy_ctor++;
}
constexpr ccm_counter(ccm_counter&& other) noexcept(std::is_nothrow_move_constructible_v<T>)
: T(other), _val_stats(other._val_stats)
{
static_assert(std::is_move_constructible_v<T>, "T must be move constructible.");
_val_stats->moves++;
_obj_stats.move_ctor++;
}
constexpr auto operator=(const ccm_counter& other) noexcept(std::is_nothrow_copy_assignable_v<T>)
-> std::enable_if_t<std::is_copy_assignable_v<T>, ccm_counter&>
{
T::operator=(other);
_val_stats = other._val_stats;
_val_stats->copies++;
_obj_stats.copy_assign++;
return *this;
}
constexpr auto operator=(ccm_counter&& other) noexcept(std::is_nothrow_move_assignable_v<T>)
-> std::enable_if_t<std::is_move_assignable_v<T>, ccm_counter&>
{
T::operator=(other);
_val_stats = other._val_stats;
_val_stats->moves++;
_obj_stats.move_assign++;
return *this;
}
[[nodiscard]] constexpr ccm_object_stats get_object_stats() const noexcept
{
return _obj_stats;
}
constexpr void set_object_stats(ccm_object_stats stats)
{
_obj_stats = std::move(stats);
}
[[nodiscard]] constexpr std::shared_ptr<ccm_value_stats> get_value_stats() const noexcept
{
return _val_stats;
}
constexpr void set_value_stats(std::shared_ptr<ccm_value_stats> stats)
{
_val_stats = std::move(stats);
}
private:
ccm_object_stats _obj_stats{};
std::shared_ptr<ccm_value_stats> _val_stats;
};
template <typename T>
std::ostream& operator<<(std::ostream& os, const ccm_counter<T>& counter)
{
return os << counter.get_ccmd_stats();
}