Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions examples/types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ struct Foo
std::vector<double> data;
};

struct NeverEmpty
{
int data;
NeverEmpty() = delete;
NeverEmpty(int d) : data(d) {};
};

struct NullableStruct { NullableStruct() {} };

struct IntDerived
Expand Down Expand Up @@ -189,6 +196,7 @@ void const_int_vec_arg(std::vector<std::shared_ptr<const int>>){}
namespace jlcxx
{
template<> struct IsMirroredType<cpp_types::DoubleData> : std::false_type { };
template<> struct IsMirroredType<cpp_types::NeverEmpty> : std::false_type { };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why, specifically, is this required? There is a general lack of documentation on what exactly a mirrored type is. This compiles fine on my local machine without disabling mirroring for this type, but fails in the runners. Why? Is disabling mirroring correct here, or is the complaint indicative of an edge case I've not considered?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because this simple test class is detected as a "plain old data" type, and by default these are expected t be mapped to a Julia struct with the same layout, by using map_type instead of add_type. To override this, IsMirroredType can be specialized. Could be better documented, indeed :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes perfect sense. Thanks for the explanation!

template<typename T> struct IsSmartPointerType<cpp_types::MySmartPointer<T>> : std::true_type { };
template<typename T> struct ConstructorPointerType<cpp_types::MySmartPointer<T>> { typedef std::shared_ptr<T> type; };
}
Expand Down Expand Up @@ -465,6 +473,14 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& types)

types.method("world_dequeue", []() { static World w; return std::deque({w}); });
types.method("world_list", []() { static World w; return std::list({w}); });

types.add_type<NeverEmpty>("NeverEmpty")
.constructor<int>()
.method("get_data", [](NeverEmpty& n) { return n.data; })
.method("set_data", [](NeverEmpty& n, int d) { n.data = d; });

types.method("neverempty_array", [] () { NeverEmpty n(1); return std::vector({n}); });
types.method("neverempty_deque", [] () { NeverEmpty n(1); return std::deque({n}); });
}

JLCXX_MODULE define_types2_module(jlcxx::Module& types2)
Expand Down
137 changes: 131 additions & 6 deletions include/jlcxx/stl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,70 @@ struct SkipIfSameAs<T,T>

template<typename T1, typename T2> using skip_if_same = typename SkipIfSameAs<T1,T2>::type;


// === is_default_insertable ===
// https://stackoverflow.com/a/78556159
template <typename, typename, typename = void>
struct is_default_insertable
: std::false_type {};

template <typename T, typename A>
struct is_default_insertable<
T, A,
std::void_t<decltype(std::allocator_traits<A>::construct(std::declval<A&>(), std::declval<T*>()))>
> : std::true_type {};

template <typename T, typename A>
inline constexpr bool is_default_insertable_v = is_default_insertable<T, A>::value;

// === is_copy_insertable ===
template <typename, typename, typename = void>
struct is_copy_insertable : std::false_type {};

template <typename T, typename A>
struct is_copy_insertable<
T, A,
std::void_t<
decltype(std::allocator_traits<A>::construct(std::declval<A&>(), std::declval<T*>(), std::declval<const T&>()))>
> : std::true_type {};

template <typename T, typename A>
inline constexpr bool is_copy_insertable_v = is_copy_insertable<T, A>::value;

// === is_move_insertable ===
template <typename, typename, typename = void>
struct is_move_insertable
: std::false_type {};

template <typename T, typename A>
struct is_move_insertable<
T, A,
std::void_t<decltype(std::allocator_traits<A>::construct(std::declval<A&>(), std::declval<T*>(), std::declval<T&&>()))>
> : std::true_type {};

template <typename T, typename A>
inline constexpr bool is_move_insertable_v = is_move_insertable<T, A>::value;

// === is_std_allocator ===
template <typename>
struct is_std_allocator : std::false_type {};

template <typename T>
struct is_std_allocator<std::allocator<T>> : std::true_type {};

template <typename T>
inline constexpr bool is_std_allocator_v = is_std_allocator<T>::value;

// === uses_std_allocator ===
template <typename C, typename = void>
struct uses_std_allocator : std::false_type {};

template <typename C>
struct uses_std_allocator<C, std::void_t<typename C::allocator_type>>
: is_std_allocator<typename C::allocator_type> {};

template <typename C>
inline constexpr bool uses_std_allocator_v = uses_std_allocator<C>::value;
}

namespace stl
Expand Down Expand Up @@ -309,17 +373,61 @@ struct WrapSTLContainer<std::vector> : STLTypeWrapperBase<WrapSTLContainer<std::
{
using WrappedT = typename TypeWrapperT::type;
using T = typename WrappedT::value_type;

// The C++ standard notes that std::vector<T>(size_t count) requires T to be
// DefaultInsertible, else behavior is undefined. Undefined behavior may
// include a compile time error (that std::is_constructable would not catch).
// - Note: This is tested to be the case for std::deque in GCC 15.2.1
// This cannot be checked in an if constexpr context prior to C++20, so we
// simply remove the size_t constructor if the type is not default insertable.
// We also check std::is_default_constructible since std's allocator will
// internally call the default constructor (which is_default_insertable
// will not check). Custom allocators may not literally call the default
// constructor, so only check if the standard allocator is used.
if constexpr(jlcxx::detail::is_default_insertable_v<T, typename WrappedT::allocator_type>
&& (!jlcxx::detail::uses_std_allocator_v<WrappedT>
|| std::is_default_constructible_v<T>))
{
wrapped.template constructor<std::size_t>();
}
// Similarly, we expose the count-copy constructor gated on CopyInsertible
if constexpr(jlcxx::detail::is_copy_insertable_v<T, typename WrappedT::allocator_type>
&& (!jlcxx::detail::uses_std_allocator_v<WrappedT>
|| std::is_copy_constructible_v<T>))
{
// Since references to Julia owned objects may be of incompatible types
// and a copy will be performed anyway over the bindings, pass by
// value into this constructor instead of reference.
using BaseType = std::remove_const_t<std::remove_reference_t<T>>;
wrapped.template constructor<std::size_t, BaseType>();
}

wrapped.module().set_override_module(stl_module());
wrapped.method("cppsize", &WrappedT::size);
wrapped.method("resize", [] (WrappedT& v, const cxxint_t s) { v.resize(s); });

// Although the C++ standard sates potential undefined behavior depending on
// if T is DefaultInsertable/MoveInsertable/CopyInsertable, the only hard
// type requirement for WrappedT<T>::resize is that if the current size
// is less than the target resize, that T must be DefaultConstructable.
if constexpr(std::is_default_constructible_v<T>)
{
wrapped.method("resize", [] (WrappedT& v, const cxxint_t s) { v.resize(s); });
}

// The exposed `append' method uses WrappedT<T>::reserve, which requires
// T to be MoveInsertible into WrappedT<T>. If this is not satisfied,
// we can simply fall back to the slower non-reserved method.
wrapped.method("append", [] (WrappedT& v, jlcxx::ArrayRef<T> arr)
{
const std::size_t addedlen = arr.size();
v.reserve(v.size() + addedlen);
for(size_t i = 0; i != addedlen; ++i)
if constexpr(jlcxx::detail::is_move_insertable_v<T, typename WrappedT::allocator_type>)
{
v.push_back(arr[i]);
v.reserve(v.size() + addedlen);
}
for(size_t i = 0; i != addedlen; ++i)
{
v.push_back(arr[i]);
}
});
wrapped.module().unset_override_module();
WrapVectorImpl<T>::wrap(wrapped);
Expand Down Expand Up @@ -364,10 +472,27 @@ struct WrapSTLContainer<std::deque> : STLTypeWrapperBase<WrapSTLContainer<std::d
using T = typename WrappedT::value_type;

wrap_range_based_bsearch(wrapped);
wrapped.template constructor<std::size_t>();
// Similar to the std::vector check, we gate the additional constructors:
if constexpr(jlcxx::detail::is_default_insertable_v<T, typename WrappedT::allocator_type>
&& (!jlcxx::detail::uses_std_allocator_v<WrappedT>
|| std::is_default_constructible_v<T>))
{
wrapped.template constructor<std::size_t>();
}
if constexpr(jlcxx::detail::is_copy_insertable_v<T, typename WrappedT::allocator_type>
&& (!jlcxx::detail::uses_std_allocator_v<WrappedT>
|| std::is_copy_constructible_v<T>))
{
using BaseType = std::remove_const_t<std::remove_reference_t<T>>;
wrapped.template constructor<std::size_t, BaseType>();
}
wrapped.module().set_override_module(stl_module());
wrapped.method("cppsize", &WrappedT::size);
wrapped.method("resize", [](WrappedT &v, const cxxint_t s) { v.resize(s); });
// Similar to the std::vector check, std::deque::resize requires DefaultConstrutable:
if constexpr(std::is_default_constructible_v<T>)
{
wrapped.method("resize", [](WrappedT &v, const cxxint_t s) { v.resize(s); });
}
wrapped.method("cxxgetindex", [](const WrappedT& v, cxxint_t i) -> const_reftype<WrappedT> { return v[i - 1]; });
wrapped.method("cxxsetindex!", [](WrappedT& v, const_reftype<WrappedT> val, cxxint_t i) { v[i - 1] = val; });
wrapped.method("push_back!", [] (WrappedT& v, const_reftype<WrappedT> val) { v.push_back(val); });
Expand Down
Loading