Skip to content

Commit adbc811

Browse files
committed
Use stricter brace initialization
This updates the `py::init` constructors to only use brace initialization for aggregate initiailization if there is no constructor with the given arguments. This, in particular, fixes the regression in #1247 where the presence of a `std::initializer_list<T>` constructor started being invoked for constructor invocations in 2.2 even when there was a specific constructor of the desired type. The added test case demonstrates: without this change, it fails to compile because the `.def(py::init<std::vector<int>>())` constructor tries to invoke the `T(std::initializer_list<std::vector<int>>)` constructor rather than the `T(std::vector<int>)` constructor. By only using `new T{...}`-style construction when a `T(...)` constructor doesn't exist, we should bypass this by while still allowing `py::init<...>` to be used for aggregate type initialization (since such types, by definition, don't have a user-declared constructor).
1 parent 326deef commit adbc811

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

include/pybind11/detail/init.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ bool is_alias(Cpp<Class> *ptr) {
5252
template <typename /*Class*/>
5353
constexpr bool is_alias(void *) { return false; }
5454

55+
// Constructs and returns a new object; if the given arguments don't map to a constructor, we fall
56+
// back to brace aggregate initiailization so that for aggregate initialization can be used with
57+
// py::init, e.g. `py::init<int, int>` to initialize a `struct T { int a; int b; }`. For
58+
// non-aggregate types, we need to use an ordinary T(...) constructor (invoking as `T{...}` usually
59+
// works, but will not do the expected thing when `T` has an `initializer_list<T>` constructor).
60+
template <typename Class, typename... Args, detail::enable_if_t<std::is_constructible<Class, Args...>::value, int> = 0>
61+
inline Class *construct_or_initialize(Args &&...args) { return new Class(std::forward<Args>(args)...); }
62+
template <typename Class, typename... Args, detail::enable_if_t<!std::is_constructible<Class, Args...>::value, int> = 0>
63+
inline Class *construct_or_initialize(Args &&...args) { return new Class{std::forward<Args>(args)...}; }
64+
5565
// Attempts to constructs an alias using a `Alias(Cpp &&)` constructor. This allows types with
5666
// an alias to provide only a single Cpp factory function as long as the Alias can be
5767
// constructed from an rvalue reference of the base Cpp type. This means that Alias classes
@@ -161,7 +171,7 @@ struct constructor {
161171
template <typename Class, typename... Extra, enable_if_t<!Class::has_alias, int> = 0>
162172
static void execute(Class &cl, const Extra&... extra) {
163173
cl.def("__init__", [](value_and_holder &v_h, Args... args) {
164-
v_h.value_ptr() = new Cpp<Class>{std::forward<Args>(args)...};
174+
v_h.value_ptr() = construct_or_initialize<Cpp<Class>>(std::forward<Args>(args)...);
165175
}, is_new_style_constructor(), extra...);
166176
}
167177

@@ -171,9 +181,9 @@ struct constructor {
171181
static void execute(Class &cl, const Extra&... extra) {
172182
cl.def("__init__", [](value_and_holder &v_h, Args... args) {
173183
if (Py_TYPE(v_h.inst) == v_h.type->type)
174-
v_h.value_ptr() = new Cpp<Class>{std::forward<Args>(args)...};
184+
v_h.value_ptr() = construct_or_initialize<Cpp<Class>>(std::forward<Args>(args)...);
175185
else
176-
v_h.value_ptr() = new Alias<Class>{std::forward<Args>(args)...};
186+
v_h.value_ptr() = construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
177187
}, is_new_style_constructor(), extra...);
178188
}
179189

@@ -182,7 +192,7 @@ struct constructor {
182192
!std::is_constructible<Cpp<Class>, Args...>::value, int> = 0>
183193
static void execute(Class &cl, const Extra&... extra) {
184194
cl.def("__init__", [](value_and_holder &v_h, Args... args) {
185-
v_h.value_ptr() = new Alias<Class>{std::forward<Args>(args)...};
195+
v_h.value_ptr() = construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
186196
}, is_new_style_constructor(), extra...);
187197
}
188198
};
@@ -193,7 +203,7 @@ template <typename... Args> struct alias_constructor {
193203
enable_if_t<Class::has_alias && std::is_constructible<Alias<Class>, Args...>::value, int> = 0>
194204
static void execute(Class &cl, const Extra&... extra) {
195205
cl.def("__init__", [](value_and_holder &v_h, Args... args) {
196-
v_h.value_ptr() = new Alias<Class>{std::forward<Args>(args)...};
206+
v_h.value_ptr() = construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
197207
}, is_new_style_constructor(), extra...);
198208
}
199209
};

tests/test_class.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
#include "pybind11_tests.h"
1111
#include "constructor_stats.h"
1212
#include "local_bindings.h"
13+
#include <pybind11/stl.h>
14+
15+
// test_brace_initialization
16+
struct NoBraceInitialization {
17+
NoBraceInitialization(std::vector<int> v) : vec{std::move(v)} {}
18+
template <typename T>
19+
NoBraceInitialization(std::initializer_list<T> l) : vec(l) {}
20+
21+
std::vector<int> vec;
22+
};
1323

1424
TEST_SUBMODULE(class_, m) {
1525
// test_instance
@@ -299,6 +309,12 @@ TEST_SUBMODULE(class_, m) {
299309
.def(py::init<int, const std::string &>())
300310
.def_readwrite("field1", &BraceInitialization::field1)
301311
.def_readwrite("field2", &BraceInitialization::field2);
312+
// We *don't* want to construct using braces when the given constructor argument maps to a
313+
// constructor, because brace initialization could go to the wrong place (in particular when
314+
// there is also an `initializer_list<T>`-accept constructor):
315+
py::class_<NoBraceInitialization>(m, "NoBraceInitialization")
316+
.def(py::init<std::vector<int>>())
317+
.def_readonly("vec", &NoBraceInitialization::vec);
302318

303319
// test_reentrant_implicit_conversion_failure
304320
// #1035: issue with runaway reentrant implicit conversion

tests/test_class.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ def test_brace_initialization():
228228
assert a.field1 == 123
229229
assert a.field2 == "test"
230230

231+
# Tests that a non-simple class doesn't get brace initialization (if the
232+
# class defines an initializer_list constructor, in particular, it would
233+
# win over the expected constructor).
234+
b = m.NoBraceInitialization([123, 456])
235+
assert b.vec == [123, 456]
236+
231237

232238
@pytest.unsupported_on_pypy
233239
def test_class_refcount():

0 commit comments

Comments
 (0)