Aggregate like Initialization without superfluous calls of constructors
Asked Answered
S

1

9

I am trying to write mostly a reimplementation of std::array. But, I am not very happy about the rules for aggregate initialization (and some other minor details) and thus do not want to use it.

Things I do not like with aggregate initilization:

  • Having less than the necessary elements in the list, produces no error, this is particularly strange for aggregates of aggregates, e.g.
    struct A { int x,y; };
    std::array< A, 2 > arr{1,2,3};
    // produces A[0].x = 1, A[0].y = 2, A[1].x = 1, A[1].y = 0
    
  • private/deleted ctors are still callable by the user
    class B { B() = delete; };
    B{};  // no compile time error
    

What I have so far is the following:

#include <cstddef>
#include <type_traits>
#include <utility>

template< typename T, typename ... Ts >
inline constexpr bool areT_v = std::conjunction_v< std::is_same<T, Ts> ... >;  

namespace ttt {
    template< typename T, int N >
    struct array
    {
        template< typename ... L > 
        array( L && ... lst ) : content{ std::forward< L >( lst ) ... } {
            static_assert( sizeof...(L) == N || sizeof...(L)==0, ""  );
            static_assert( areT_v<T,L...>, "" );
        }
        array() = default;  // not sure whether I need this one
    
        using ContentType = typename std::conditional< N>=1 , T, char >::type;
        ContentType content[ N>=1 ? N : 1];

        /* member functions */
    };
}

Actually I am quite happy with this implementation, apart from the following fact. Using ttt::array I have more ctor calls then when using std::array. E.g.:

#include <array>
#include <iostream>

struct CLASS {
    int x, y;
    CLASS() { 
        std::cout << "def-ctor" << std::endl; 
    }
    CLASS(int x_,int y_) : x(x_), y(y_) { 
        std::cout << "xy-ctor" << std::endl; 
    }
    CLASS( CLASS const & other ) : x(other.x), y(other.y) {
        std::cout << "copy-ctor" << std::endl; 
    }
};

int main() {
    std::array< CLASS, 1 > stda{CLASS{1,2}};  // calls xy-ctor
    ttt::array< CLASS, 1 > ttta{CLASS{1,2}};  // calls xy-ctor AND copy-ctor
}

This seems to be connected with the reason that std::array is aggregate initialized (because it is an aggregate), but ttt::array is not. This leads me to the question, is the compiler allowed to do something here (omitting one ctor call), I am not allowed to do?

And, is there a way around this?

Slemmer answered 7/2, 2022 at 10:42 Comment(3)
"private/deleted ctors are still callable by the user" has been fixed in C++20 gcc.godbolt.org/z/9b9Wvn3fjTight
An aggregate is, in a sense, transparent to the compiler; the compiler can initialize its members directly, there's no constructor that needs to be called and can do something special. As your class is not an aggregate, the calling code has no choice but to construct a temporary CLASS and pass it to ttt::array constructor (which then has no choice but to copy or move it into content); it can't bypass the constructor and write the CLASS instance directly into content array. I don't think you can avoid copying without making ttt::array an aggregate (which would defeat the point).Urian
Issue 1 is detected by compiler as warning: godbolt.org/z/a6Wobrsqe. IMO proper configuration of compiler is better approach then creating new array.Cineaste
S
1

You could package the arguments that should be used to construct the elements in content in std::tuples and delay creation of the actual objects until it's time to initialize content.

An outline:

template <class T, std::size_t N>
struct array {
    template<class... Ts>
    constexpr array(Ts&&... args) :
        content{to_t(std::forward<Ts>(args),
                     std::make_index_sequence<std::tuple_size<Ts>::value>())...}
    {
        static_assert(sizeof...(Ts) == N, "wrong number of elements");
    }

private:
    // create one T from a tuple
    template<class... Ts, std::size_t... Idx>
    T to_t(std::tuple<Ts...>&& in, std::index_sequence<Idx...>) {
        return T{std::forward<Ts>(std::get<Idx>(in))...};
    }

    T content[N];
};

Example type to put in an array:

struct Foo {
    Foo() : a(0.), b(0) { std::cout << "Foo::Foo()\n"; }
    explicit Foo(double A, int B) : a(A), b(B) { std::cout << "Foo::Foo(double, int)\n"; }
    Foo(const Foo& o) : a(o.a), b(o.b) { std::cout << "Foo(const Foo&)\n"; }
    Foo(Foo&& o) : a(o.a), b(o.b) { std::cout << "Foo(Foo&&)\n"; }
    ~Foo() { std::cout << "Foo::~Foo() {" << a << ", " << b << "}\n"; }

private:
    double a;
    int b;
};

You could then create your array without copying/moving your T:s (guaranteed in C++17, with copy/move elision in C++14, so not guaranteed but probable) by packaging them in tuples. It's a bit cumbersome to type. Hopefully someone else has an idea for how to get around that.

int main() {
    Foo f{3.3, 3};
    array<Foo, 4> x(std::forward_as_tuple(1.1, 1),
                    std::forward_as_tuple(),            // default constructor
                    std::forward_as_tuple(2.2, 2),
                    std::forward_as_tuple(std::move(f)) // move constructor
                   );
}

Demo
You can add the compiler option -fno-elide-constructors to see that it will move the elements when that optimization is turned off in C++14 mode.

Sandstone answered 7/2, 2022 at 17:16 Comment(2)
do I understand correctly that this is something like emplace_back, but for all arguments given at once?Slemmer
@Slemmer Yes, that is what I was aiming for. I think it could be made nicer with a helper function. I'll think more about that and update the answer if I come up with anythingSandstone

© 2022 - 2024 — McMap. All rights reserved.