constructor_stats.h revision 14299
1#pragma once
2/*
3    tests/constructor_stats.h -- framework for printing and tracking object
4    instance lifetimes in example/test code.
5
6    Copyright (c) 2016 Jason Rhinelander <jason@imaginary.ca>
7
8    All rights reserved. Use of this source code is governed by a
9    BSD-style license that can be found in the LICENSE file.
10
11This header provides a few useful tools for writing examples or tests that want to check and/or
12display object instance lifetimes.  It requires that you include this header and add the following
13function calls to constructors:
14
15    class MyClass {
16        MyClass() { ...; print_default_created(this); }
17        ~MyClass() { ...; print_destroyed(this); }
18        MyClass(const MyClass &c) { ...; print_copy_created(this); }
19        MyClass(MyClass &&c) { ...; print_move_created(this); }
20        MyClass(int a, int b) { ...; print_created(this, a, b); }
21        MyClass &operator=(const MyClass &c) { ...; print_copy_assigned(this); }
22        MyClass &operator=(MyClass &&c) { ...; print_move_assigned(this); }
23
24        ...
25    }
26
27You can find various examples of these in several of the existing testing .cpp files.  (Of course
28you don't need to add any of the above constructors/operators that you don't actually have, except
29for the destructor).
30
31Each of these will print an appropriate message such as:
32
33    ### MyClass @ 0x2801910 created via default constructor
34    ### MyClass @ 0x27fa780 created 100 200
35    ### MyClass @ 0x2801910 destroyed
36    ### MyClass @ 0x27fa780 destroyed
37
38You can also include extra arguments (such as the 100, 200 in the output above, coming from the
39value constructor) for all of the above methods which will be included in the output.
40
41For testing, each of these also keeps track the created instances and allows you to check how many
42of the various constructors have been invoked from the Python side via code such as:
43
44    from pybind11_tests import ConstructorStats
45    cstats = ConstructorStats.get(MyClass)
46    print(cstats.alive())
47    print(cstats.default_constructions)
48
49Note that `.alive()` should usually be the first thing you call as it invokes Python's garbage
50collector to actually destroy objects that aren't yet referenced.
51
52For everything except copy and move constructors and destructors, any extra values given to the
53print_...() function is stored in a class-specific values list which you can retrieve and inspect
54from the ConstructorStats instance `.values()` method.
55
56In some cases, when you need to track instances of a C++ class not registered with pybind11, you
57need to add a function returning the ConstructorStats for the C++ class; this can be done with:
58
59    m.def("get_special_cstats", &ConstructorStats::get<SpecialClass>, py::return_value_policy::reference)
60
61Finally, you can suppress the output messages, but keep the constructor tracking (for
62inspection/testing in python) by using the functions with `print_` replaced with `track_` (e.g.
63`track_copy_created(this)`).
64
65*/
66
67#include "pybind11_tests.h"
68#include <unordered_map>
69#include <list>
70#include <typeindex>
71#include <sstream>
72
73class ConstructorStats {
74protected:
75    std::unordered_map<void*, int> _instances; // Need a map rather than set because members can shared address with parents
76    std::list<std::string> _values; // Used to track values (e.g. of value constructors)
77public:
78    int default_constructions = 0;
79    int copy_constructions = 0;
80    int move_constructions = 0;
81    int copy_assignments = 0;
82    int move_assignments = 0;
83
84    void copy_created(void *inst) {
85        created(inst);
86        copy_constructions++;
87    }
88
89    void move_created(void *inst) {
90        created(inst);
91        move_constructions++;
92    }
93
94    void default_created(void *inst) {
95        created(inst);
96        default_constructions++;
97    }
98
99    void created(void *inst) {
100        ++_instances[inst];
101    }
102
103    void destroyed(void *inst) {
104        if (--_instances[inst] < 0)
105            throw std::runtime_error("cstats.destroyed() called with unknown "
106                                     "instance; potential double-destruction "
107                                     "or a missing cstats.created()");
108    }
109
110    static void gc() {
111        // Force garbage collection to ensure any pending destructors are invoked:
112#if defined(PYPY_VERSION)
113        PyObject *globals = PyEval_GetGlobals();
114        PyObject *result = PyRun_String(
115            "import gc\n"
116            "for i in range(2):"
117            "    gc.collect()\n",
118            Py_file_input, globals, globals);
119        if (result == nullptr)
120            throw py::error_already_set();
121        Py_DECREF(result);
122#else
123        py::module::import("gc").attr("collect")();
124#endif
125    }
126
127    int alive() {
128        gc();
129        int total = 0;
130        for (const auto &p : _instances)
131            if (p.second > 0)
132                total += p.second;
133        return total;
134    }
135
136    void value() {} // Recursion terminator
137    // Takes one or more values, converts them to strings, then stores them.
138    template <typename T, typename... Tmore> void value(const T &v, Tmore &&...args) {
139        std::ostringstream oss;
140        oss << v;
141        _values.push_back(oss.str());
142        value(std::forward<Tmore>(args)...);
143    }
144
145    // Move out stored values
146    py::list values() {
147        py::list l;
148        for (const auto &v : _values) l.append(py::cast(v));
149        _values.clear();
150        return l;
151    }
152
153    // Gets constructor stats from a C++ type index
154    static ConstructorStats& get(std::type_index type) {
155        static std::unordered_map<std::type_index, ConstructorStats> all_cstats;
156        return all_cstats[type];
157    }
158
159    // Gets constructor stats from a C++ type
160    template <typename T> static ConstructorStats& get() {
161#if defined(PYPY_VERSION)
162        gc();
163#endif
164        return get(typeid(T));
165    }
166
167    // Gets constructor stats from a Python class
168    static ConstructorStats& get(py::object class_) {
169        auto &internals = py::detail::get_internals();
170        const std::type_index *t1 = nullptr, *t2 = nullptr;
171        try {
172            auto *type_info = internals.registered_types_py.at((PyTypeObject *) class_.ptr()).at(0);
173            for (auto &p : internals.registered_types_cpp) {
174                if (p.second == type_info) {
175                    if (t1) {
176                        t2 = &p.first;
177                        break;
178                    }
179                    t1 = &p.first;
180                }
181            }
182        }
183        catch (const std::out_of_range &) {}
184        if (!t1) throw std::runtime_error("Unknown class passed to ConstructorStats::get()");
185        auto &cs1 = get(*t1);
186        // If we have both a t1 and t2 match, one is probably the trampoline class; return whichever
187        // has more constructions (typically one or the other will be 0)
188        if (t2) {
189            auto &cs2 = get(*t2);
190            int cs1_total = cs1.default_constructions + cs1.copy_constructions + cs1.move_constructions + (int) cs1._values.size();
191            int cs2_total = cs2.default_constructions + cs2.copy_constructions + cs2.move_constructions + (int) cs2._values.size();
192            if (cs2_total > cs1_total) return cs2;
193        }
194        return cs1;
195    }
196};
197
198// To track construction/destruction, you need to call these methods from the various
199// constructors/operators.  The ones that take extra values record the given values in the
200// constructor stats values for later inspection.
201template <class T> void track_copy_created(T *inst) { ConstructorStats::get<T>().copy_created(inst); }
202template <class T> void track_move_created(T *inst) { ConstructorStats::get<T>().move_created(inst); }
203template <class T, typename... Values> void track_copy_assigned(T *, Values &&...values) {
204    auto &cst = ConstructorStats::get<T>();
205    cst.copy_assignments++;
206    cst.value(std::forward<Values>(values)...);
207}
208template <class T, typename... Values> void track_move_assigned(T *, Values &&...values) {
209    auto &cst = ConstructorStats::get<T>();
210    cst.move_assignments++;
211    cst.value(std::forward<Values>(values)...);
212}
213template <class T, typename... Values> void track_default_created(T *inst, Values &&...values) {
214    auto &cst = ConstructorStats::get<T>();
215    cst.default_created(inst);
216    cst.value(std::forward<Values>(values)...);
217}
218template <class T, typename... Values> void track_created(T *inst, Values &&...values) {
219    auto &cst = ConstructorStats::get<T>();
220    cst.created(inst);
221    cst.value(std::forward<Values>(values)...);
222}
223template <class T, typename... Values> void track_destroyed(T *inst) {
224    ConstructorStats::get<T>().destroyed(inst);
225}
226template <class T, typename... Values> void track_values(T *, Values &&...values) {
227    ConstructorStats::get<T>().value(std::forward<Values>(values)...);
228}
229
230/// Don't cast pointers to Python, print them as strings
231inline const char *format_ptrs(const char *p) { return p; }
232template <typename T>
233py::str format_ptrs(T *p) { return "{:#x}"_s.format(reinterpret_cast<std::uintptr_t>(p)); }
234template <typename T>
235auto format_ptrs(T &&x) -> decltype(std::forward<T>(x)) { return std::forward<T>(x); }
236
237template <class T, typename... Output>
238void print_constr_details(T *inst, const std::string &action, Output &&...output) {
239    py::print("###", py::type_id<T>(), "@", format_ptrs(inst), action,
240              format_ptrs(std::forward<Output>(output))...);
241}
242
243// Verbose versions of the above:
244template <class T, typename... Values> void print_copy_created(T *inst, Values &&...values) { // NB: this prints, but doesn't store, given values
245    print_constr_details(inst, "created via copy constructor", values...);
246    track_copy_created(inst);
247}
248template <class T, typename... Values> void print_move_created(T *inst, Values &&...values) { // NB: this prints, but doesn't store, given values
249    print_constr_details(inst, "created via move constructor", values...);
250    track_move_created(inst);
251}
252template <class T, typename... Values> void print_copy_assigned(T *inst, Values &&...values) {
253    print_constr_details(inst, "assigned via copy assignment", values...);
254    track_copy_assigned(inst, values...);
255}
256template <class T, typename... Values> void print_move_assigned(T *inst, Values &&...values) {
257    print_constr_details(inst, "assigned via move assignment", values...);
258    track_move_assigned(inst, values...);
259}
260template <class T, typename... Values> void print_default_created(T *inst, Values &&...values) {
261    print_constr_details(inst, "created via default constructor", values...);
262    track_default_created(inst, values...);
263}
264template <class T, typename... Values> void print_created(T *inst, Values &&...values) {
265    print_constr_details(inst, "created", values...);
266    track_created(inst, values...);
267}
268template <class T, typename... Values> void print_destroyed(T *inst, Values &&...values) { // Prints but doesn't store given values
269    print_constr_details(inst, "destroyed", values...);
270    track_destroyed(inst);
271}
272template <class T, typename... Values> void print_values(T *inst, Values &&...values) {
273    print_constr_details(inst, ":", values...);
274    track_values(inst, values...);
275}
276
277