Skip to content

Instantly share code, notes, and snippets.

@trueqbit
Last active January 23, 2020 08:18
Show Gist options
  • Save trueqbit/820392a869d071aaf811a320cb02267f to your computer and use it in GitHub Desktop.
Save trueqbit/820392a869d071aaf811a320cb02267f to your computer and use it in GitHub Desktop.
C++17: Experiment with a C array wrapper container that allows construction/assignment from a C array and from arrays differing in type
#pragma once
#include <type_traits>
#include <cstddef>
#include <memory>
#include <utility>
#include <algorithm>
using namespace std;
enum class array2_value_init_tag {};
enum class array2_default_init_tag {};
inline constexpr array2_value_init_tag array2_value_init{};
inline constexpr array2_default_init_tag array2_default_init{};
template<typename T, size_t N>
struct caggregate
{
using carray_t = T[N];
using carray_ct = const T[N];
carray_t elems_;
};
/** Encapsulates a C array as an aggregate, but defers its initialization.
* Defers initialization of the underlying aggregate (by means of a union).
This allows derived classes to provide user-defined constructors.
* Direct aggregate initialization is mimicked by a variadic constructor.
Note: The default is to value-initialize instead of to default-initialize.
The reason for this is: In order to make direct-initialization possible,
the proxy has to provide a variadic constructor, following a user-defined
default-constructor, which in turn makes it impossible to differentiate between
no-init default initialization and explicit value-initialization. Default-initialization can be requested explicitly
(which can be seen as a security enhancement).
Distinct specializations for different properties of the underlying element's type are available.
The technical implementation uses 2 layers of indirection:
1) Defer+Control automatic aggregate handling by means of a union.
2) Benefit from automatic aggregate handling by placing a named aggregate into the union.
3) Manually provide constructors/destructors/copy+move operators.
*/
template<
typename T, size_t N,
bool HasTrivialDefaultInit = is_trivially_default_constructible_v<T>,
bool HasTrivialDtor = is_trivially_destructible_v<T>
>
struct array2_proxy;
// no specialization for:
// - HasTrivialDefaultInit = true
// - HasTrivialDtor = false
template<typename T, size_t N>
struct array2_proxy<T, N, true, false>;
// partial specialization for:
// - HasTrivialDefaultInit = true
// - HasTrivialDtor = true/false
// means:
// - trivial default initialization (which allows the aggregate to remain uninitialized)
// - default dtor
template<typename T, size_t N, bool HasTrivialDtor>
struct array2_proxy<T, N, true, HasTrivialDtor>
{
using aggregate = caggregate<T, N>;
~array2_proxy() = default;
// emulate default initialization
// (can never be constexpr)
array2_proxy(array2_default_init_tag)
{}
// emulate value initialization
// (default ctor)
constexpr array2_proxy(array2_value_init_tag = {}) :
a{}
{}
// emulate direct initialization
template<
typename First, typename... Rest,
enable_if_t<is_constructible_v<T, First&&>, int> = 0
>
constexpr array2_proxy(First&& first, Rest&&... rest) :
a{ forward<First>(first), forward<Rest>(rest)... }
{}
// upstream direct initialization
constexpr array2_proxy(aggregate&& a) :
a{ move(a) }
{}
constexpr array2_proxy(const array2_proxy& other) = default;
constexpr array2_proxy(array2_proxy&& other) = default;
constexpr array2_proxy& operator =(const array2_proxy& other) = default;
constexpr array2_proxy& operator =(array2_proxy&& other) = default;
aggregate a;
protected:
// no-init
// (can never be constexpr)
array2_proxy(std::nullptr_t)
{}
};
// specialization for:
// - HasTrivialDefaultInit = false
// - HasTrivialDtor = false
// means:
// - non-trivial default initialization
// - non-trivial dtor
template<typename T, size_t N>
struct array2_proxy<T, N, false, false>
{
using aggregate = caggregate<T, N>;
~array2_proxy()
{
a.~aggregate();
}
// emulate default initialization
array2_proxy(array2_default_init_tag):
a{}
{}
// emulate value initialization
// (default ctor)
array2_proxy(array2_value_init_tag = {}):
a{}
{}
// emulate direct initialization
template<
typename First, typename... Rest,
enable_if_t<is_constructible_v<T, First&&>, int> = 0
>
array2_proxy(First&& first, Rest&&... rest) :
a{ std::forward<First>(first), std::forward<Rest>(rest)... }
{}
// upstream direct initialization
array2_proxy(aggregate&& a) :
a{ move(a) }
{}
array2_proxy(const array2_proxy& other) :
a{ other.a }
{}
array2_proxy(array2_proxy&& other) :
a{ move(other.a) }
{}
array2_proxy& operator =(const array2_proxy& other)
{
a = other.a;
return *this;
}
array2_proxy& operator =(array2_proxy&& other)
{
a = move(other.a);
return *this;
}
// prevent automatic code generation
union
{
// benefit from generated aggregate operations
aggregate a;
};
protected:
// no-init
// (can never be constexpr)
array2_proxy(std::nullptr_t)
{}
};
// specialization for:
// - bool HasTrivialDefaultInit = false
// - bool HasTrivialDtor = true
// means:
// - non-trivial default initialization
// - trivial dtor
template<typename T, size_t N>
struct array2_proxy<T, N, false, true>
{
using aggregate = caggregate<T, N>;
~array2_proxy() = default;
// emulate default initialization
constexpr array2_proxy(array2_default_init_tag):
a{}
{}
// emulate value initialization
// (default ctor)
constexpr array2_proxy(array2_value_init_tag = {}):
a{}
{}
// emulate direct initialization
template<
typename First, typename... Rest,
enable_if_t<is_constructible_v<T, First&&>, int> = 0
>
constexpr array2_proxy(First&& first, Rest&&... rest) :
a{ std::forward<First>(first), std::forward<Rest>(rest)... }
{}
// upstream direct initialization
constexpr array2_proxy(aggregate&& a) :
a{ move(a) }
{}
constexpr array2_proxy(const array2_proxy& other) :
a{ other.a }
{}
constexpr array2_proxy(array2_proxy&& other) :
a{ move(other.a) }
{}
constexpr array2_proxy& operator =(const array2_proxy& other)
{
a = other.a;
return *this;
}
constexpr array2_proxy& operator =(array2_proxy&& other)
{
a = move(other.a);
return *this;
}
// prevent automatic code generation
union
{
// benefit from generated aggregate operations
aggregate a;
};
protected:
// no-init
// (can never be constexpr)
array2_proxy(std::nullptr_t)
{}
};
template<typename T, size_t N>
class array2;
template<typename T, size_t N>
class array2: public array2_proxy<T, N>
{
using base_t = array2_proxy<T, N>;
public:
using aggregate = typename base_t::aggregate;
using carray_t = typename aggregate::carray_t;
using carray_ct = typename aggregate::carray_ct;
using value_type = T;
using size_type = size_t;
using difference_type = ptrdiff_t;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using base_t::base_t;
array2(const array2&) = default;
array2(array2&&) = default;
array2& operator =(const array2&) = default;
array2& operator =(array2&&) = default;
// copy-construct from same array
array2(carray_ct right) :
base_t{ nullptr }
{
uninitialized_copy_n(right, N, this->a.elems_);
}
// move-construct from same array
array2(carray_t&& right) :
base_t{ nullptr }
{
uninitialized_move_n(right, N, this->a.elems_);
}
// copy-assign from same array
array2& operator =(carray_ct right)
{
copy_n(right, N, this->a.elems_);
return *this;
}
// move-assign from same array
array2& operator =(carray_t&& right)
{
move(right, right + N, this->a.elems_);
return *this;
}
// copy-construct from any array of same size
template<
typename U,
enable_if_t<is_constructible_v<T, const U&>, int> = 0
>
array2(const array2<U, N>& other):
base_t{ nullptr }
{
uninitialized_copy_n(other.a.elems_, N, this->a.elems_);
}
// move-construct from any array of same size
template<
typename U,
enable_if_t<is_constructible_v<T, U&&>, int> = 0
>
array2(array2<U, N>&& other) :
base_t{ nullptr }
{
uninitialized_move_n(other.a.elems_, N, this->a.elems_);
}
// copy-assign from any array of same size
template<
typename U,
enable_if_t<is_convertible_v<T, const U&>, int> = 0
>
array2& operator =(const array2& other)
{
if (addressof(other) != this)
copy_n(other.a.elems_, N, this->a.elems_);
return *this;
}
// move-assign from any array of same size
template<
typename U,
enable_if_t<is_convertible_v<T, U&&>, int> = 0
>
array2& operator =(array2&& other)
{
if (addressof(other) != this)
move(other.a.elems_, other.a.elems_ + N, this->a.elems_);
return *this;
}
// copy-construct from any C array
template<
typename U, size_t M,
enable_if_t<is_constructible_v<T, U&>, int> = 0
>
explicit array2(U (&right)[M]) :
base_t{ nullptr }
{
static_assert(M <= N);
uninitialized_copy_n(right, min<>(M, N), this->a.elems_);
// value-construct remaining elements N-M
if constexpr (M < N)
uninitialized_value_construct_n(this->a.elems_ + M, N - M);
}
// copy-construct from any C array
template<
typename U, size_t M,
enable_if_t<is_constructible_v<T, U&&>, int> = 0
>
explicit array2(U (&&right)[M]) :
base_t{ nullptr }
{
static_assert(M <= N);
uninitialized_move_n(right, min<>(M, N), this->a.elems_);
// value-construct remaining elements N-M
if constexpr (M < N)
uninitialized_value_construct_n(this->a.elems_ + M, N - M);
}
// copy-assign from any C array of same size
template<
typename U,
enable_if_t<is_assignable_v<T&, U&>, int> = 0
>
array2& operator =(U (&right)[N])
{
copy_n(right, N, this->a.elems_);
return *this;
}
// copy-assign from any C array of same size
template<
typename U,
enable_if_t<is_assignable_v<T&, U&&>, int> = 0
>
array2& operator =(U (&&right)[N])
{
move(right, right + N, this->a.elems_);
return *this;
}
[[nodiscard]] constexpr size_type size() const noexcept
{
return N;
}
[[nodiscard]] constexpr size_type max_size() const noexcept
{
return N;
}
[[nodiscard]] constexpr bool empty() const noexcept
{
return false;
}
[[nodiscard]] constexpr reference operator [](size_t i) noexcept
{
return this->a.elems_[i];
}
[[nodiscard]] constexpr const_reference operator [](size_t i) const noexcept
{
return this->a.elems_[i];
}
[[nodiscard]] constexpr reference front() noexcept
{
return this->a.elems_[0];
}
[[nodiscard]] constexpr const_reference front() const noexcept
{
return this->a.elems_[0];
}
[[nodiscard]] constexpr reference back() noexcept
{
return this->a.elems_[N - 1];
}
[[nodiscard]] constexpr const_reference back() const noexcept
{
return this->a.elems_[N - 1];
}
[[nodiscard]] constexpr T* data() noexcept
{
return this->a.elems_;
}
[[nodiscard]] constexpr const T* data() const noexcept
{
return this->a.elems_;
}
};
// class template parameter deduction guides
template <typename First, typename... Rest>
caggregate(First&&, Rest&&...)->caggregate<First, 1u + sizeof...(Rest)>;
template <typename First, typename... Rest>
array2(First&&, Rest&&...)->array2<remove_cv_t<remove_reference_t<First>>, 1u + sizeof...(Rest)>;
template <typename T, size_t N>
array2(caggregate<T, N>&&)->array2<T, N>;
template <typename T, size_t N>
array2(T(&)[N])->array2<T, N>;
template <typename T, size_t N>
array2(const T(&)[N])->array2<T, N>;
#include <string>
#include <iostream>
#include <ios>
#include "array2.h"
using namespace std::string_literals;
int main()
{
struct S
{
constexpr S() = default;
constexpr S(int i) : x{ i } {}
int x = 4;
};
cout << "HasTrivialDefaultInit<int>: " << boolalpha << (is_trivially_default_constructible_v<int>) << "\n";
cout << "HasTrivialDtor<int>: " << boolalpha << (is_trivially_destructible_v<int>) << "\n";
cout << "HasTrivialDefaultInit<string>: " << boolalpha << (is_trivially_default_constructible_v<string>) << "\n";
cout << "HasTrivialDtor<string>: " << boolalpha << (is_trivially_destructible_v<string>) << "\n";
cout << "HasTrivialDefaultInit<S>: " << boolalpha << (is_trivially_default_constructible_v<S>) << "\n";
cout << "HasTrivialDtor<S>: " << boolalpha << (is_trivially_destructible_v<S>) << "\n";
{
int acarray3[3] = { 1, 2, 3 };
int acarray2[] = { 4, 5 };
array2<int, 3> a0;
array2 a1{ 1, 2, 3 };
array2<int, 3> a2 = { 1, 2, 3 };
array2<int, 3> a3{ acarray3 };
a3 = acarray3;
array2<int, 3> a5{ acarray2 };
array2 ca{ caggregate{1} };
array2<long, 3> l0{ a0 };
l0 = a0;
array2<long, 3> l1{ move(acarray3) };
l1 = move(acarray3);
// impossible
//constexpr array2<int, 1> defaultconstexprint(array2_default_init);
constexpr array2_proxy<int, 1> oneinttest{ /*array2_value_init*/ };
constexpr array2<int, 1> oneint{ /*array2_value_init*/ };
// gcc/clang choke on this? msvc works...
//constexpr const int* i = oneint.data();
//constexpr const int& x = oneint[0];
constexpr array2<int, 3> threeints{ 1, 2 };
array2 deduced1{ 1, 2, 3 };
array2 deduced2{ oneint };
array2 deduced3 = { 1, 2, 3 };
array2 deduced4 = oneint;
array2 deduced5{ acarray3 };
array2 deduced6 = acarray3;
}
{
S acarray3[3] = { 1, 2, 3 };
S acarray2[] = { 4, 5 };
array2<S, 3> a0;
array2 a1{ S(1), S(2), S(3) };
array2<S, 3> a2 = { 1, 2, 3 };
array2<S, 3> a3{ acarray3 };
a3 = acarray3;
array2<S, 3> a5{ acarray2 };
array2 ca{ caggregate{S{}} };
// impossible
//constexpr array2<S, 1> defaultconstexprint(array2_default_init);
constexpr array2<S, 1> one;
//constexpr const S* i = one.data();
//constexpr const S& x = one[0];
constexpr array2<S, 3> three{ 1, 2 };
array2 deduced1{ 1, 2, 3 };
array2 deduced2{ one };
array2 deduced3 = { 1, 2, 3 };
array2 deduced4 = one;
array2 deduced5{ acarray3 };
array2 deduced6 = acarray3;
}
{
string acarray3[3] = { "1", "2", "3" };
string acarray2[] = { "4", "5" };
array2<string, 3> a0;
array2<string, 3> a1{ "1", "2", "3" };
array2<string, 3> a2 = { "1", "2", "3" };
array2<string, 3> a3{ acarray3 };
a3 = acarray3;
array2<string, 3> a5{ acarray2 };
array2 ca{ caggregate{""s} };
array2<const char*, 3> l00{ "1", "2", "3" };
array2<string, 3> l0{ l00 };
l0 = l00;
// impossible
//constexpr array2<string, 1> oneint(array2_value_init);
//constexpr const string* i = oneint.data();
array2<string, 1> one;
array2 deduced1{ "1"s, "2"s, "3"s };
array2 deduced2{ one };
array2 deduced3 = { "1"s, "2"s, "3"s };
array2 deduced4 = one;
array2 deduced5{ acarray3 };
array2 deduced6 = acarray3;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment