Commit 3f589379 authored by Jason Rhinelander's avatar Jason Rhinelander
Browse files

Improve constructor/destructor tracking

This commit rewrites the examples that look for constructor/destructor
calls to do so via static variable tracking rather than output parsing.

The added ConstructorStats class provides methods to keep track of
constructors and destructors, number of default/copy/move constructors,
and number of copy/move assignments.  It also provides a mechanism for
storing values (e.g. for value construction), and then allows all of
this to be checked at the end of a test by getting the statistics for a
C++ (or python mapping) class.

By not relying on the precise pattern of constructions/destructions,
but rather simply ensuring that every construction is matched with a
destruction on the same object, we ensure that everything that gets
created also gets destroyed as expected.

This replaces all of the various "std::cout << whatever" code in
constructors/destructors with
`print_created(this)`/`print_destroyed(this)`/etc. functions which
provide similar output, but now has a unified format across the
different examples, including a new ### prefix that makes mixed example
output and lifecycle events easier to distinguish.

With this change, relaxed mode is no longer needed, which enables
testing for proper destruction under MSVC, and under any other compiler
that generates code calling extra constructors, or optimizes away any
constructors.  GCC/clang are used as the baseline for move
constructors; the tests are adapted to allow more move constructors to
be evoked (but other types are constructors much have matching counts).

This commit also disables output buffering of tests, as the buffering
sometimes results in C++ output ending up in the middle of python
output (or vice versa), depending on the OS/python version.
parent 85557b1d
......@@ -65,3 +65,10 @@ print("__name__(example.ExamplePythonTypes) = %s" % ExamplePythonTypes.__name__)
print("__module__(example.ExamplePythonTypes) = %s" % ExamplePythonTypes.__module__)
print("__name__(example.ExamplePythonTypes.get_set) = %s" % ExamplePythonTypes.get_set.__name__)
print("__module__(example.ExamplePythonTypes.get_set) = %s" % ExamplePythonTypes.get_set.__module__)
from example import ConstructorStats
cstats = ConstructorStats.get(ExamplePythonTypes)
print("Instances not destroyed:", cstats.alive())
instance = None
print("Instances not destroyed:", cstats.alive())
......@@ -2,6 +2,7 @@
5
example.ExamplePythonTypes: No constructor defined!
can't set attribute
### ExamplePythonTypes @ 0x1045b80 created via new_instance
key: key2, value=value2
key: key, value=value
key: key, value=value
......@@ -134,4 +135,6 @@ __name__(example.ExamplePythonTypes) = ExamplePythonTypes
__module__(example.ExamplePythonTypes) = example
__name__(example.ExamplePythonTypes.get_set) = get_set
__module__(example.ExamplePythonTypes.get_set) = example
Destructing ExamplePythonTypes
Instances not destroyed: 1
### ExamplePythonTypes @ 0x1045b80 destroyed
Instances not destroyed: 0
......@@ -9,51 +9,55 @@
*/
#include "example.h"
#include "constructor-stats.h"
#include <pybind11/operators.h>
#include <pybind11/stl.h>
class Sequence {
public:
Sequence(size_t size) : m_size(size) {
std::cout << "Value constructor: Creating a sequence with " << m_size << " entries" << std::endl;
print_created(this, "of size", m_size);
m_data = new float[size];
memset(m_data, 0, sizeof(float) * size);
}
Sequence(const std::vector<float> &value) : m_size(value.size()) {
std::cout << "Value constructor: Creating a sequence with " << m_size << " entries" << std::endl;
print_created(this, "of size", m_size, "from std::vector");
m_data = new float[m_size];
memcpy(m_data, &value[0], sizeof(float) * m_size);
}
Sequence(const Sequence &s) : m_size(s.m_size) {
std::cout << "Copy constructor: Creating a sequence with " << m_size << " entries" << std::endl;
print_copy_created(this);
m_data = new float[m_size];
memcpy(m_data, s.m_data, sizeof(float)*m_size);
}
Sequence(Sequence &&s) : m_size(s.m_size), m_data(s.m_data) {
std::cout << "Move constructor: Creating a sequence with " << m_size << " entries" << std::endl;
print_move_created(this);
s.m_size = 0;
s.m_data = nullptr;
}
~Sequence() {
std::cout << "Freeing a sequence with " << m_size << " entries" << std::endl;
print_destroyed(this);
delete[] m_data;
}
Sequence &operator=(const Sequence &s) {
std::cout << "Assignment operator: Creating a sequence with " << s.m_size << " entries" << std::endl;
delete[] m_data;
m_size = s.m_size;
m_data = new float[m_size];
memcpy(m_data, s.m_data, sizeof(float)*m_size);
if (&s != this) {
delete[] m_data;
m_size = s.m_size;
m_data = new float[m_size];
memcpy(m_data, s.m_data, sizeof(float)*m_size);
}
print_copy_assigned(this);
return *this;
}
Sequence &operator=(Sequence &&s) {
std::cout << "Move assignment operator: Creating a sequence with " << s.m_size << " entries" << std::endl;
if (&s != this) {
delete[] m_data;
m_size = s.m_size;
......@@ -61,6 +65,9 @@ public:
s.m_size = 0;
s.m_data = nullptr;
}
print_move_assigned(this);
return *this;
}
......
......@@ -28,3 +28,19 @@ rev[0::2] = Sequence([2.0, 2.0, 2.0])
for i in rev:
print(i, end=' ')
print('')
from example import ConstructorStats
cstats = ConstructorStats.get(Sequence)
print("Instances not destroyed:", cstats.alive())
s = None
print("Instances not destroyed:", cstats.alive())
rev = None
print("Instances not destroyed:", cstats.alive())
rev2 = None
print("Instances not destroyed:", cstats.alive())
print("Constructor values:", cstats.values())
print("Default constructions:", cstats.default_constructions)
print("Copy constructions:", cstats.copy_constructions)
print("Move constructions:", cstats.move_constructions >= 1)
print("Copy assignments:", cstats.copy_assignments)
print("Move assignments:", cstats.move_assignments)
Value constructor: Creating a sequence with 5 entries
s = <example.Sequence object at 0x10c786c70>
### Sequence @ 0x1535b00 created of size 5
s = <example.Sequence object at 0x7efc73cfa4e0>
len(s) = 5
s[0], s[3] = 0.000000 0.000000
12.34 in s: False
12.34 in s: True
s[0], s[3] = 12.340000 56.779999
Value constructor: Creating a sequence with 5 entries
Move constructor: Creating a sequence with 5 entries
Freeing a sequence with 0 entries
Value constructor: Creating a sequence with 5 entries
### Sequence @ 0x7fff22a45068 created of size 5
### Sequence @ 0x1538b90 created via move constructor
### Sequence @ 0x7fff22a45068 destroyed
### Sequence @ 0x1538bf0 created of size 5
rev[0], rev[1], rev[2], rev[3], rev[4] = 0.000000 56.779999 0.000000 0.000000 12.340000
0.0 56.7799987793 0.0 0.0 12.3400001526
0.0 56.7799987793 0.0 0.0 12.3400001526
0.0 56.779998779296875 0.0 0.0 12.34000015258789
0.0 56.779998779296875 0.0 0.0 12.34000015258789
True
Value constructor: Creating a sequence with 3 entries
Freeing a sequence with 3 entries
2.0 56.7799987793 2.0 0.0 2.0
Freeing a sequence with 5 entries
Freeing a sequence with 5 entries
Freeing a sequence with 5 entries
### Sequence @ 0x153c4b0 created of size 3 from std::vector
### Sequence @ 0x153c4b0 destroyed
2.0 56.779998779296875 2.0 0.0 2.0
Instances not destroyed: 3
### Sequence @ 0x1535b00 destroyed
Instances not destroyed: 2
### Sequence @ 0x1538b90 destroyed
Instances not destroyed: 1
### Sequence @ 0x1538bf0 destroyed
Instances not destroyed: 0
Constructor values: ['of size', '5', 'of size', '5', 'of size', '5', 'of size', '3', 'from std::vector']
Default constructions: 0
Copy constructions: 0
Move constructions: True
Copy assignments: 0
Move assignments: 0
......@@ -15,7 +15,7 @@
class MyObject1 : public Object {
public:
MyObject1(int value) : value(value) {
std::cout << toString() << " constructor" << std::endl;
print_created(this, toString());
}
std::string toString() const {
......@@ -24,7 +24,7 @@ public:
protected:
virtual ~MyObject1() {
std::cout << toString() << " destructor" << std::endl;
print_destroyed(this);
}
private:
......@@ -35,7 +35,7 @@ private:
class MyObject2 {
public:
MyObject2(int value) : value(value) {
std::cout << toString() << " constructor" << std::endl;
print_created(this, toString());
}
std::string toString() const {
......@@ -43,7 +43,7 @@ public:
}
virtual ~MyObject2() {
std::cout << toString() << " destructor" << std::endl;
print_destroyed(this);
}
private:
......@@ -54,7 +54,7 @@ private:
class MyObject3 : public std::enable_shared_from_this<MyObject3> {
public:
MyObject3(int value) : value(value) {
std::cout << toString() << " constructor" << std::endl;
print_created(this, toString());
}
std::string toString() const {
......@@ -62,7 +62,7 @@ public:
}
virtual ~MyObject3() {
std::cout << toString() << " destructor" << std::endl;
print_destroyed(this);
}
private:
......@@ -144,4 +144,7 @@ void init_ex_smart_ptr(py::module &m) {
m.def("print_myobject3_4", &print_myobject3_4);
py::implicitly_convertible<py::int_, MyObject1>();
// Expose constructor stats for the ref type
m.def("cstats_ref", &ConstructorStats::get<ref_tag>);
}
......@@ -68,3 +68,18 @@ for o in [MyObject3(9), make_myobject3_1(), make_myobject3_2()]:
print_myobject3_2(o)
print_myobject3_3(o)
print_myobject3_4(o)
from example import ConstructorStats, cstats_ref, Object
cstats = [ConstructorStats.get(Object), ConstructorStats.get(MyObject1),
ConstructorStats.get(MyObject2), ConstructorStats.get(MyObject3),
cstats_ref()]
print("Instances not destroyed:", [x.alive() for x in cstats])
o = None
print("Instances not destroyed:", [x.alive() for x in cstats])
print("Object value constructions:", [x.values() for x in cstats])
print("Default constructions:", [x.default_constructions for x in cstats])
print("Copy constructions:", [x.copy_constructions for x in cstats])
#print("Move constructions:", [x.move_constructions >= 0 for x in cstats]) # Doesn't invoke any
print("Copy assignments:", [x.copy_assignments for x in cstats])
print("Move assignments:", [x.move_assignments for x in cstats])
MyObject1[1] constructor
Initialized ref from pointer 0x1347ba0
MyObject1[2] constructor
Initialized ref from pointer 0x12b9270
Initialized ref from ref 0x12b9270
Destructing ref 0x12b9270
MyObject1[3] constructor
Initialized ref from pointer 0x12a2a90
### Object @ 0xdeffd0 created via default constructor
### MyObject1 @ 0xdeffd0 created MyObject1[1]
### ref<MyObject1> @ 0x7f6a2e03c4a8 created from pointer 0xdeffd0
### Object @ 0xe43f50 created via default constructor
### MyObject1 @ 0xe43f50 created MyObject1[2]
### ref<Object> @ 0x7fff136845d0 created from pointer 0xe43f50
### ref<MyObject1> @ 0x7f6a2c32aad8 created via copy constructor with pointer 0xe43f50
### ref<Object> @ 0x7fff136845d0 destroyed
### Object @ 0xee8cf0 created via default constructor
### MyObject1 @ 0xee8cf0 created MyObject1[3]
### ref<MyObject1> @ 0x7f6a2c32ab08 created from pointer 0xee8cf0
Reference count = 1
MyObject1[1]
Created empty ref
Assigning ref 0x1347ba0
Initialized ref from ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xdeffd0
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xdeffd0
MyObject1[1]
Destructing ref 0x1347ba0
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xdeffd0
MyObject1[1]
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xdeffd0
MyObject1[1]
Destructing ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 destroyed
Reference count = 1
MyObject1[2]
Created empty ref
Assigning ref 0x12b9270
Initialized ref from ref 0x12b9270
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xe43f50
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xe43f50
MyObject1[2]
Destructing ref 0x12b9270
Destructing ref 0x12b9270
Created empty ref
Assigning ref 0x12b9270
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xe43f50
MyObject1[2]
Destructing ref 0x12b9270
Created empty ref
Assigning ref 0x12b9270
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xe43f50
MyObject1[2]
Destructing ref 0x12b9270
### ref<Object> @ 0x7fff136845c8 destroyed
Reference count = 1
MyObject1[3]
Created empty ref
Assigning ref 0x12a2a90
Initialized ref from ref 0x12a2a90
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8cf0
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xee8cf0
MyObject1[3]
Destructing ref 0x12a2a90
Destructing ref 0x12a2a90
Created empty ref
Assigning ref 0x12a2a90
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8cf0
MyObject1[3]
Destructing ref 0x12a2a90
Created empty ref
Assigning ref 0x12a2a90
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8cf0
MyObject1[3]
Destructing ref 0x12a2a90
Destructing ref 0x12b9270
MyObject1[2] destructor
Destructing ref 0x1347ba0
MyObject1[1] destructor
MyObject1[4] constructor
Initialized ref from pointer 0x1347ba0
MyObject1[5] constructor
Initialized ref from pointer 0x1299190
Initialized ref from ref 0x1299190
Destructing ref 0x1299190
MyObject1[6] constructor
Initialized ref from pointer 0x133e2f0
Destructing ref 0x12a2a90
MyObject1[3] destructor
### ref<Object> @ 0x7fff136845c8 destroyed
### MyObject1 @ 0xe43f50 destroyed
### Object @ 0xe43f50 destroyed
### ref<MyObject1> @ 0x7f6a2c32aad8 destroyed
### MyObject1 @ 0xdeffd0 destroyed
### Object @ 0xdeffd0 destroyed
### ref<MyObject1> @ 0x7f6a2e03c4a8 destroyed
### Object @ 0xee8310 created via default constructor
### MyObject1 @ 0xee8310 created MyObject1[4]
### ref<MyObject1> @ 0x7f6a2e03c4a8 created from pointer 0xee8310
### Object @ 0xee8470 created via default constructor
### MyObject1 @ 0xee8470 created MyObject1[5]
### ref<MyObject1> @ 0x7fff136845d0 created from pointer 0xee8470
### ref<MyObject1> @ 0x7f6a2c32aad8 created via copy constructor with pointer 0xee8470
### ref<MyObject1> @ 0x7fff136845d0 destroyed
### Object @ 0xee95a0 created via default constructor
### MyObject1 @ 0xee95a0 created MyObject1[6]
### ref<MyObject1> @ 0x7f6a2c32ab38 created from pointer 0xee95a0
### MyObject1 @ 0xee8cf0 destroyed
### Object @ 0xee8cf0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab08 destroyed
<example.MyObject1 object at 0x7f6a2e03c480>
MyObject1[4]
Created empty ref
Assigning ref 0x1347ba0
Initialized ref from ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
### ref<Object> @ 0x7fff136845c8 destroyed
MyObject1[4]
Created empty ref
Assigning ref 0x1347ba0
Initialized ref from ref 0x1347ba0
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
### ref<MyObject1> @ 0x7fff136845a8 created via copy constructor with pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<MyObject1> @ 0x7fff136845a8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
Created empty ref
Assigning ref 0x1347ba0
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8310
MyObject1[4]
Destructing ref 0x1347ba0
### ref<MyObject1> @ 0x7fff136845c8 destroyed
<example.MyObject1 object at 0x7f6a2c32aab0>
MyObject1[5]
Created empty ref
Assigning ref 0x1299190
Initialized ref from ref 0x1299190
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
Destructing ref 0x1299190
Created empty ref
Assigning ref 0x1299190
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
Created empty ref
Assigning ref 0x1299190
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
### ref<Object> @ 0x7fff136845c8 destroyed
MyObject1[5]
Created empty ref
Assigning ref 0x1299190
Initialized ref from ref 0x1299190
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
### ref<MyObject1> @ 0x7fff136845a8 created via copy constructor with pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
Destructing ref 0x1299190
Created empty ref
Assigning ref 0x1299190
### ref<MyObject1> @ 0x7fff136845a8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
Created empty ref
Assigning ref 0x1299190
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee8470
MyObject1[5]
Destructing ref 0x1299190
### ref<MyObject1> @ 0x7fff136845c8 destroyed
<example.MyObject1 object at 0x7f6a2c32ab10>
MyObject1[6]
Created empty ref
Assigning ref 0x133e2f0
Initialized ref from ref 0x133e2f0
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
### ref<Object> @ 0x7fff136845a8 created via copy constructor with pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
Destructing ref 0x133e2f0
Created empty ref
Assigning ref 0x133e2f0
### ref<Object> @ 0x7fff136845a8 destroyed
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
Created empty ref
Assigning ref 0x133e2f0
### ref<Object> @ 0x7fff136845c8 destroyed
### ref<Object> @ 0x7fff136845c8 created via default constructor
### ref<Object> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
### ref<Object> @ 0x7fff136845c8 destroyed
MyObject1[6]
Created empty ref
Assigning ref 0x133e2f0
Initialized ref from ref 0x133e2f0
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
### ref<MyObject1> @ 0x7fff136845a8 created via copy constructor with pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
Destructing ref 0x133e2f0
Created empty ref
Assigning ref 0x133e2f0
### ref<MyObject1> @ 0x7fff136845a8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
Created empty ref
Assigning ref 0x133e2f0
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee95a0
MyObject1[6]
Destructing ref 0x133e2f0
MyObject1[7] constructor
Initialized ref from pointer 0x133f3a0
### ref<MyObject1> @ 0x7fff136845c8 destroyed
7
### Object @ 0xee97f0 created via default constructor
### MyObject1 @ 0xee97f0 created MyObject1[7]
### ref<MyObject1> @ 0x7f6a2c32ab08 created from pointer 0xee97f0
MyObject1[7]
Destructing ref 0x133f3a0
MyObject1[7] destructor
Created empty ref
MyObject1[7] constructor
Initialized ref from pointer 0x12a2a90
Assigning ref 0x12a2a90
Initialized ref from ref 0x12a2a90
### MyObject1 @ 0xee97f0 destroyed
### Object @ 0xee97f0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab08 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### Object @ 0xee99e0 created via default constructor
### MyObject1 @ 0xee99e0 created MyObject1[7]
### ref<MyObject1> @ 0x7f6a2c32ab08 created from pointer 0xee99e0
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee99e0
### ref<MyObject1> @ 0x7fff136845a8 created via copy constructor with pointer 0xee99e0
MyObject1[7]
Destructing ref 0x12a2a90
Destructing ref 0x12a2a90
Destructing ref 0x12a2a90
MyObject1[7] destructor
Created empty ref
MyObject1[7] constructor
Initialized ref from pointer 0x133f3a0
Assigning ref 0x133f3a0
### ref<MyObject1> @ 0x7fff136845a8 destroyed
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### MyObject1 @ 0xee99e0 destroyed
### Object @ 0xee99e0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab08 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### Object @ 0xee97f0 created via default constructor
### MyObject1 @ 0xee97f0 created MyObject1[7]
### ref<MyObject1> @ 0x7f6a2c32ab08 created from pointer 0xee97f0
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee97f0
MyObject1[7]
Destructing ref 0x133f3a0
Destructing ref 0x133f3a0
MyObject1[7] destructor
Created empty ref
MyObject1[7] constructor
Initialized ref from pointer 0x12a2a90
Assigning ref 0x12a2a90
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### MyObject1 @ 0xee97f0 destroyed
### Object @ 0xee97f0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab08 destroyed
### ref<MyObject1> @ 0x7fff136845c8 created via default constructor
### Object @ 0xee99e0 created via default constructor
### MyObject1 @ 0xee99e0 created MyObject1[7]
### ref<MyObject1> @ 0x7f6a2c32ab08 created from pointer 0xee99e0
### ref<MyObject1> @ 0x7fff136845c8 assigned via copy assignment pointer 0xee99e0
MyObject1[7]
Destructing ref 0x12a2a90
Destructing ref 0x12a2a90
MyObject1[7] destructor
Destructing ref 0x133e2f0
MyObject1[6] destructor
Destructing ref 0x1299190
MyObject1[5] destructor
Destructing ref 0x1347ba0
MyObject1[4] destructor
MyObject2[8] constructor
MyObject2[6] constructor
MyObject2[7] constructor
### ref<MyObject1> @ 0x7fff136845c8 destroyed
### MyObject1 @ 0xee99e0 destroyed
### Object @ 0xee99e0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab08 destroyed
### MyObject1 @ 0xee95a0 destroyed
### Object @ 0xee95a0 destroyed
### ref<MyObject1> @ 0x7f6a2c32ab38 destroyed
### MyObject1 @ 0xee8470 destroyed
### Object @ 0xee8470 destroyed
### ref<MyObject1> @ 0x7f6a2c32aad8 destroyed
### MyObject1 @ 0xee8310 destroyed
### Object @ 0xee8310 destroyed
### ref<MyObject1> @ 0x7f6a2e03c4a8 destroyed
### MyObject2 @ 0xe43f50 created MyObject2[8]
### MyObject2 @ 0xee95a0 created MyObject2[6]
### MyObject2 @ 0xee95d0 created MyObject2[7]
<example.MyObject2 object at 0x7f6a2dfc8768>
MyObject2[8]
MyObject2[8]
MyObject2[8]
MyObject2[8]
<example.MyObject2 object at 0x7f6a2dfc86c0>
MyObject2[6]
MyObject2[6]
MyObject2[6]
MyObject2[6]
<example.MyObject2 object at 0x7f6a2c32d030>
MyObject2[7]
MyObject2[7]
MyObject2[7]
MyObject2[7]
MyObject2[6] destructor
MyObject2[8] destructor
MyObject3[9] constructor
MyObject3[8] constructor
MyObject3[9] constructor
MyObject2[7] destructor
### MyObject2 @ 0xee95a0 destroyed
### MyObject2 @ 0xe43f50 destroyed
### MyObject3 @ 0xee9ac0 created MyObject3[9]
### MyObject3 @ 0xe43f90 created MyObject3[8]
### MyObject3 @ 0xeea7d0 created MyObject3[9]
### MyObject2 @ 0xee95d0 destroyed
<example.MyObject3 object at 0x7f6a2dfc8768>
MyObject3[9]
MyObject3[9]
MyObject3[9]
MyObject3[9]
<example.MyObject3 object at 0x7f6a2dfc86c0>
MyObject3[8]
MyObject3[8]
MyObject3[8]
MyObject3[8]
<example.MyObject3 object at 0x7f6a2c32d068>
MyObject3[9]
MyObject3[9]
MyObject3[9]
MyObject3[9]
MyObject3[8] destructor
MyObject3[9] destructor
Reference count = 1
Reference count = 1
Reference count = 1
<example.MyObject1 object at 0x7f830b500e68>
<example.MyObject1 object at 0x7f830b4fc688>
<example.MyObject1 object at 0x7f830b4fc5a8>
7
<example.MyObject2 object at 0x7f830b50b330>
<example.MyObject2 object at 0x7f830b50bdb0>
<example.MyObject2 object at 0x7f83098f6330>
<example.MyObject3 object at 0x7f830b50b330>
<example.MyObject3 object at 0x7f830b50bdb0>
<example.MyObject3 object at 0x7f83098f6370>
MyObject3[9] destructor
### MyObject3 @ 0xe43f90 destroyed
### MyObject3 @ 0xee9ac0 destroyed
Instances not destroyed: [0, 0, 0, 1, 0]
### MyObject3 @ 0xeea7d0 destroyed
Instances not destroyed: [0, 0, 0, 0, 0]
Object value constructions: [[], ['MyObject1[1]', 'MyObject1[2]', 'MyObject1[3]', 'MyObject1[4]', 'MyObject1[5]', 'MyObject1[6]', 'MyObject1[7]', 'MyObject1[7]', 'MyObject1[7]', 'MyObject1[7]'], ['MyObject2[8]', 'MyObject2[6]', 'MyObject2[7]'], ['MyObject3[9]', 'MyObject3[8]', 'MyObject3[9]'], ['from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer', 'from pointer']]
Default constructions: [10, 0, 0, 0, 30]
Copy constructions: [0, 0, 0, 0, 12]
Copy assignments: [0, 0, 0, 0, 30]
Move assignments: [0, 0, 0, 0, 0]
......@@ -8,18 +8,16 @@
*/
#include "example.h"
#include "constructor-stats.h"
#include <pybind11/functional.h>
/* This is an example class that we'll want to be able to extend from Python */
class ExampleVirt {
public:
ExampleVirt(int state) : state(state) {
cout << "Constructing ExampleVirt.." << endl;
}
~ExampleVirt() {
cout << "Destructing ExampleVirt.." << endl;
}
ExampleVirt(int state) : state(state) { print_created(this, state); }
ExampleVirt(const ExampleVirt &e) : state(e.state) { print_copy_created(this); }
ExampleVirt(ExampleVirt &&e) : state(e.state) { print_move_created(this); e.state = 0; }
~ExampleVirt() { print_destroyed(this); }
virtual int run(int value) {
std::cout << "Original implementation of ExampleVirt::run(state=" << state
......@@ -71,8 +69,8 @@ public:
class NonCopyable {
public:
NonCopyable(int a, int b) : value{new int(a*b)} {}
NonCopyable(NonCopyable &&) = default;
NonCopyable(int a, int b) : value{new int(a*b)} { print_created(this, a, b); }
NonCopyable(NonCopyable &&o) { value = std::move(o.value); print_move_created(this); }
NonCopyable(const NonCopyable &) = delete;
NonCopyable() = delete;
void operator=(const NonCopyable &) = delete;
......@@ -80,7 +78,7 @@ public:
std::string get_value() const {
if (value) return std::to_string(*value); else return "(null)";
}
~NonCopyable() { std::cout << "NonCopyable destructor @ " << this << "; value = " << get_value() << std::endl; }
~NonCopyable() { print_destroyed(this); }
private:
std::unique_ptr<int> value;
......@@ -90,11 +88,11 @@ private:
// when it is not referenced elsewhere, but copied if it is still referenced.
class Movable {
public:
Movable(int a, int b) : value{a+b} {}
Movable(const Movable &m) { value = m.value; std::cout << "Movable @ " << this << " copy constructor" << std::endl; }
Movable(Movable &&m) { value = std::move(m.value); std::cout << "Movable @ " << this << " move constructor" << std::endl; }
Movable(int a, int b) : value{a+b} { print_created(this, a, b); }
Movable(const Movable &m) { value = m.value; print_copy_created(this); }
Movable(Movable &&m) { value = std::move(m.value); print_move_created(this); }
int get_value() const { return value; }
~Movable() { std::cout << "Movable destructor @ " << this << "; value = " << get_value() << std::endl; }
~Movable() { print_destroyed(this); }
private:
int value;
};
......@@ -305,5 +303,6 @@ void init_ex_virtual_functions(py::module &m) {
m.def("runExampleVirtBool", &runExampleVirtBool);
m.def("runExampleVirtVirtual", &runExampleVirtVirtual);
m.def("cstats_debug", &ConstructorStats::get<ExampleVirt>);
initialize_inherited_virtuals(m);
}
......@@ -37,8 +37,6 @@ print(runExampleVirt(ex12p, 20))
print(runExampleVirtBool(ex12p))
runExampleVirtVirtual(ex12p)
sys.stdout.flush()
class VI_AR(A_Repeat):
def unlucky_number(self):
return 99
......@@ -122,3 +120,16 @@ try:
except RuntimeError as e:
# Don't print the exception message here because it differs under debug/non-debug mode
print("Caught expected exception")
from example import ConstructorStats
del ex12
del ex12p
del obj
del ncv1
del ncv2
cstats = [ConstructorStats.get(ExampleVirt), ConstructorStats.get(NonCopyable), ConstructorStats.get(Movable)]
print("Instances not destroyed:", [x.alive() for x in cstats])
print("Constructor values:", [x.values() for x in cstats])
print("Copy constructions:", [x.copy_constructions for x in cstats])
print("Move constructions:", [cstats[i].move_constructions >= 1 for i in range(1, len(cstats))])
Constructing ExampleVirt..
### ExampleVirt @ 0x2073a90 created 10
Original implementation of ExampleVirt::run(state=10, value=20)
30
Caught expected exception: Tried to call pure virtual function "ExampleVirt::pure_virtual"
Constructing ExampleVirt..
### ExampleVirt @ 0x2076a00 created 11
ExtendedExampleVirt::run(20), calling parent..
Original implementation of ExampleVirt::run(state=11, value=21)
32
......@@ -78,20 +78,29 @@ VI_DT says: quack quack quack
Unlucky = 1234
Lucky = -4.25
2^2 * 3^2 =
NonCopyable destructor @ 0x1a6c3f0; value = (null)
### NonCopyable @ 0x207df10 created 4 9
### NonCopyable @ 0x7ffcfe866228 created via move constructor
### NonCopyable @ 0x207df10 destroyed
36
NonCopyable destructor @ 0x7ffc6d1fbaa8; value = 36
### NonCopyable @ 0x7ffcfe866228 destroyed
4 + 5 =
Movable @ 0x7ffc6d1fbacc copy constructor
### Movable @ 0x207e230 created 4 5
### Movable @ 0x7ffcfe86624c created via copy constructor
9
Movable destructor @ 0x7ffc6d1fbacc; value = 9
### Movable @ 0x7ffcfe86624c destroyed
7 + 7 =
Movable @ 0x7ffc6d1fbacc move constructor
Movable destructor @ 0x1a6c4d0; value = 14
### Movable @ 0x20259e0 created 7 7
### Movable @ 0x7ffcfe86624c created via move constructor
### Movable @ 0x20259e0 destroyed
14
Movable destructor @ 0x7ffc6d1fbacc; value = 14
### Movable @ 0x7ffcfe86624c destroyed
### NonCopyable @ 0x2025a00 created 9 9
Caught expected exception
NonCopyable destructor @ 0x29a64b0; value = 81
Movable destructor @ 0x1a6c410; value = 9
Destructing ExampleVirt..
Destructing ExampleVirt..
### ExampleVirt @ 0x2073a90 destroyed
### ExampleVirt @ 0x2076a00 destroyed
### Movable @ 0x207e230 destroyed
### NonCopyable @ 0x2025a00 destroyed
Instances not destroyed: [0, 0, 0]
Constructor values: [['10', '11'], ['4', '9', '9', '9'], ['4', '5', '7', '7']]
Copy constructions: [0, 0, 1]
Move constructions: [True, True]
......@@ -8,6 +8,7 @@
*/
#include "example.h"
#include "constructor-stats.h"
void init_ex_methods_and_attributes(py::module &);
void init_ex_python_types(py::module &);
......@@ -34,9 +35,24 @@ void init_issues(py::module &);
void init_eigen(py::module &);
#endif
void bind_ConstructorStats(py::module &m) {
py::class_<ConstructorStats>(m, "ConstructorStats")
.def("alive", &ConstructorStats::alive)
.def("values", &ConstructorStats::values)
.def_readwrite("default_constructions", &ConstructorStats::default_constructions)
.def_readwrite("copy_assignments", &ConstructorStats::copy_assignments)
.def_readwrite("move_assignments", &ConstructorStats::move_assignments)
.def_readwrite("copy_constructions", &ConstructorStats::copy_constructions)
.def_readwrite("move_constructions", &ConstructorStats::move_constructions)
.def_static("get", (ConstructorStats &(*)(py::object)) &ConstructorStats::get, py::return_value_policy::reference_internal)
;
}
PYBIND11_PLUGIN(example) {
py::module m("example", "pybind example plugin");
bind_ConstructorStats(m);
init_ex_methods_and_attributes(m);
init_ex_python_types(m);
init_ex_operator_overloading(m);
......
......@@ -8,11 +8,18 @@
*/
#include "example.h"
#include "constructor-stats.h"
#include <pybind11/stl.h>
#include <pybind11/operators.h>
PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>);
#define TRACKERS(CLASS) CLASS() { print_default_created(this); } ~CLASS() { print_destroyed(this); }
struct NestABase { int value = -2; TRACKERS(NestABase) };
struct NestA : NestABase { int value = 3; NestA& operator+=(int i) { value += i; return *this; } TRACKERS(NestA) };
struct NestB { NestA a; int value = 4; NestB& operator-=(int i) { value -= i; return *this; } TRACKERS(NestB) };
struct NestC { NestB b; int value = 5; NestC& operator*=(int i) { value *= i; return *this; } TRACKERS(NestC) };
void init_issues(py::module &m) {
py::module m2 = m.def_submodule("issues");
......@@ -159,12 +166,6 @@ void init_issues(py::module &m) {
;
// Issue #328: first member in a class can't be used in operators
#define TRACKERS(CLASS) CLASS() { std::cout << #CLASS "@" << this << " constructor\n"; } \
~CLASS() { std::cout << #CLASS "@" << this << " destructor\n"; }
struct NestABase { int value = -2; TRACKERS(NestABase) };
struct NestA : NestABase { int value = 3; NestA& operator+=(int i) { value += i; return *this; } TRACKERS(NestA) };
struct NestB { NestA a; int value = 4; NestB& operator-=(int i) { value -= i; return *this; } TRACKERS(NestB) };
struct NestC { NestB b; int value = 5; NestC& operator*=(int i) { value *= i; return *this; } TRACKERS(NestC) };
py::class_<NestABase>(m2, "NestABase").def(py::init<>()).def_readwrite("value", &NestABase::value);
py::class_<NestA>(m2, "NestA").def(py::init<>()).def(py::self += int())
.def("as_base", [](NestA &a) -> NestABase& { return (NestABase&) a; }, py::return_value_policy::reference_internal);
......
......@@ -24,15 +24,15 @@ Failed as expected: Incompatible constructor arguments. The following argument t
1. example.issues.StrIssue(arg0: int)
2. example.issues.StrIssue()
Invoked with: no, such, constructor
NestABase@0x1152940 constructor
NestA@0x1152940 constructor
NestABase@0x11f9350 constructor
NestA@0x11f9350 constructor
NestB@0x11f9350 constructor
NestABase@0x112d0d0 constructor
NestA@0x112d0d0 constructor
NestB@0x112d0d0 constructor
NestC@0x112d0d0 constructor
### NestABase @ 0x15eb630 created via default constructor
### NestA @ 0x15eb630 created via default constructor
### NestABase @ 0x1704000 created via default constructor
### NestA @ 0x1704000 created via default constructor
### NestB @ 0x1704000 created via default constructor
### NestABase @ 0x1633110 created via default constructor
### NestA @ 0x1633110 created via default constructor
### NestB @ 0x1633110 created via default constructor
### NestC @ 0x1633110 created via default constructor
13
103
1003
......@@ -43,13 +43,13 @@ NestC@0x112d0d0 constructor
42
-2
42
NestC@0x112d0d0 destructor
NestB@0x112d0d0 destructor
NestA@0x112d0d0 destructor
NestABase@0x112d0d0 destructor
### NestC @ 0x1633110 destroyed
### NestB @ 0x1633110 destroyed
### NestA @ 0x1633110 destroyed
### NestABase @ 0x1633110 destroyed
42
NestA@0x1152940 destructor
NestABase@0x1152940 destructor
NestB@0x11f9350 destructor
NestA@0x11f9350 destructor
NestABase@0x11f9350 destructor
### NestA @ 0x15eb630 destroyed
### NestABase @ 0x15eb630 destroyed
### NestB @ 0x1704000 destroyed
### NestA @ 0x1704000 destroyed
### NestABase @ 0x1704000 destroyed
......@@ -2,15 +2,16 @@
#define __OBJECT_H
#include <atomic>
#include "constructor-stats.h"
/// Reference counted object base class
class Object {
public:
/// Default constructor
Object() { }
Object() { print_default_created(this); }
/// Copy constructor
Object(const Object &) : m_refCount(0) {}
Object(const Object &) : m_refCount(0) { print_copy_created(this); }
/// Return the current reference count
int getRefCount() const { return m_refCount; };
......@@ -37,11 +38,17 @@ protected:
/** \brief Virtual protected deconstructor.
* (Will only be called by \ref ref)
*/
virtual ~Object() { }
virtual ~Object() { print_destroyed(this); }
private:
mutable std::atomic<int> m_refCount { 0 };
};
// Tag class used to track constructions of ref objects. When we track constructors, below, we
// track and print out the actual class (e.g. ref<MyObject>), and *also* add a fake tracker for
// ref_tag. This lets us check that the total number of ref<Anything> constructors/destructors is
// correct without having to check each individual ref<Whatever> type individually.
class ref_tag {};
/**
* \brief Reference counting helper
*
......@@ -55,37 +62,43 @@ private:
template <typename T> class ref {
public:
/// Create a nullptr reference
ref() : m_ptr(nullptr) { std::cout << "Created empty ref" << std::endl; }
ref() : m_ptr(nullptr) { print_default_created(this); track_default_created((ref_tag*) this); }
/// Construct a reference from a pointer
ref(T *ptr) : m_ptr(ptr) {
std::cout << "Initialized ref from pointer " << ptr<< std::endl;
if (m_ptr) ((Object *) m_ptr)->incRef();
print_created(this, "from pointer", m_ptr); track_created((ref_tag*) this, "from pointer");
}
/// Copy constructor
ref(const ref &r) : m_ptr(r.m_ptr) {
std::cout << "Initialized ref from ref " << r.m_ptr << std::endl;
if (m_ptr)
((Object *) m_ptr)->incRef();
print_copy_created(this, "with pointer", m_ptr); track_copy_created((ref_tag*) this);
}
/// Move constructor
ref(ref &&r) : m_ptr(r.m_ptr) {
std::cout << "Initialized ref with move from ref " << r.m_ptr << std::endl;
r.m_ptr = nullptr;
print_move_created(this, "with pointer", m_ptr); track_move_created((ref_tag*) this);
}
/// Destroy this reference
~ref() {
std::cout << "Destructing ref " << m_ptr << std::endl;
if (m_ptr)
((Object *) m_ptr)->decRef();
print_destroyed(this); track_destroyed((ref_tag*) this);
}
/// Move another reference into the current one
ref& operator=(ref&& r) {
std::cout << "Move-assigning ref " << r.m_ptr << std::endl;
print_move_assigned(this, "pointer", r.m_ptr); track_move_assigned((ref_tag*) this);
if (*this == r)
return *this;
if (m_ptr)
......@@ -97,7 +110,8 @@ public:
/// Overwrite this reference with another reference
ref& operator=(const ref& r) {
std::cout << "Assigning ref " << r.m_ptr << std::endl;
print_copy_assigned(this, "pointer", r.m_ptr); track_copy_assigned((ref_tag*) this);
if (m_ptr == r.m_ptr)
return *this;
if (m_ptr)
......@@ -110,7 +124,8 @@ public:
/// Overwrite this reference with a pointer to another object
ref& operator=(T *ptr) {
std::cout << "Assigning ptr " << ptr << " to ref" << std::endl;
print_values(this, "assigned pointer"); track_values((ref_tag*) this, "assigned pointer");
if (m_ptr == ptr)
return *this;
if (m_ptr)
......
......@@ -9,14 +9,16 @@ remove_long_marker = re.compile(r'([0-9])L')
remove_hex = re.compile(r'0x[0-9a-fA-F]+')
shorten_floats = re.compile(r'([1-9][0-9]*\.[0-9]{4})[0-9]*')
relaxed = False
def sanitize(lines):
lines = lines.split('\n')
for i in range(len(lines)):
line = lines[i]
if line.startswith(" |"):
line = ""
if line.startswith("### "):
# Constructor/destructor output. Useful for example, but unreliable across compilers;
# testing of proper construction/destruction occurs with ConstructorStats mechanism instead
line = ""
line = remove_unicode_marker.sub(r'\1', line)
line = remove_long_marker.sub(r'\1', line)
line = remove_hex.sub(r'0', line)
......@@ -28,13 +30,6 @@ def sanitize(lines):
line = line.replace('example.EMode', 'EMode')
line = line.replace('method of builtins.PyCapsule instance', '')
line = line.strip()
if relaxed:
lower = line.lower()
# The precise pattern of allocations and deallocations is dependent on the compiler
# and optimization level, so we unfortunately can't reliably check it in this kind of test case
if 'constructor' in lower or 'destructor' in lower \
or 'ref' in lower or 'freeing' in lower:
line = ""
lines[i] = line
return '\n'.join(sorted([l for l in lines if l != ""]))
......@@ -44,16 +39,12 @@ if path != '':
os.chdir(path)
if len(sys.argv) < 2:
print("Syntax: %s [--relaxed] <test name>" % sys.argv[0])
print("Syntax: %s <test name>" % sys.argv[0])
exit(0)
if len(sys.argv) == 3 and sys.argv[1] == '--relaxed':
del sys.argv[1]
relaxed = True
name = sys.argv[1]
try:
output_bytes = subprocess.check_output([sys.executable, name + ".py"],
output_bytes = subprocess.check_output([sys.executable, "-u", name + ".py"],
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
if e.returncode == 99:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment