6

I was expecting this code to create 10 elements vector each constructed as A{1, 2, 3.0}, but in reality the output is 1

#include <iostream>
#include <vector>

struct A {
    int a{0};
    int b{0};
    double c{0.0};
};

template<typename T, size_t... Ns>
auto create_array(std::index_sequence<Ns...>, auto&&... args) {
    return std::vector<T> {
        ([&](...){ return std::forward<T>(T{args...}); }(Ns), ...)
    };
}

template<typename T, size_t N>
auto create_array(auto&&... args) {
    return create_array<T>(std::make_index_sequence<N>{}, std::forward<decltype(args)>(args)...);
}

int main() {
    std::cout << create_array<A, 10>(1,2,3.0).size() << std::endl;
}

How could this happen, why did not it unfold correctly? What is wrong here? In particular I wanted this line

([&](...){ return std::forward<T>(T{args...}); }(Ns), ...)

to become

[&](...){ return std::forward<T>(T{args...}); }(0),
[&](...){ return std::forward<T>(T{args...}); }(1),
[&](...){ return std::forward<T>(T{args...}); }(2),
//...
Dmitry
  • 1,065
  • 7
  • 15

4 Answers4

5

This

return std::vector<T> {
    ([&](...){ return std::forward<T>(T{args...}); }(Ns), ...)
};

Unpacks to

return std::vector<T> {
    ([&](...){ return std::forward<T>(T{args...}); }(0),
     [&](...){ return std::forward<T>(T{args...}); }(1),
     [&](...){ return std::forward<T>(T{args...}); }(2),
     /* etc */)
};

Which is a parenthesized chain of comma operators, which evaluates to the last expression in sequence. You want to simply unpack into the vectors arguments, without the fold expression:

return std::vector<T> {
    [&](...){ return std::forward<T>(T{args...}); }(Ns)...
};
yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
2

The line you mention:

([&](...){ return std::forward<T>(T{args...}); }(Ns), ...)

is a fold expression. It only returns the last value because that's how the comma operator works. Remove the parentheses and the comma to instead expand the expression with Ns into an initializer list:

template<typename T, size_t... Ns>
auto create_array(std::index_sequence<Ns...>, auto&&... args) {
    return std::vector<T> {
        [&](...){ return std::forward<T>(T{args...}); }(Ns)...
    };
}
Nelfeal
  • 12,593
  • 1
  • 20
  • 39
0

I see four issues:

  • The fold over the comma operator discards all but the last instance, so you only get one. You need to do a pack expansion instead.
  • Using auto&&... as a parameter declaration is not valid C++17.
  • The use of std::forward is pointless. If left in the code, someone might feel tempted to later change it into
    return T{std::forward<Args>(args)...};
    
    which could make the first instantiated T leave args... in a valid, but indeterminate state. std::strings could be emptied etc. Just remove std::forward.
  • You are creating a std::vector<T> instead of a std::array<T, N>.

Corrected:

template <class T, size_t... Ns, class... Args>
constexpr auto create_array(std::index_sequence<Ns...>, Args&&... args) {
    return std::array{
        [&](std::size_t) { return T{args...}; }(Ns)...
    };
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
0

The actual problem might be more complicated, but in this particular case you could simply use a constructor that takes a number of elements as a parameter:

template<typename T, size_t N, class... Args>
auto create_array(Args&&... args) {
    return std::vector<T>(N, T{std::forward<Args>(args)...});
}
Evg
  • 25,259
  • 5
  • 41
  • 83
  • 1
    Yes, I put `vector` just for simplicity, in reality I'd like to be able to create a `constexpr std::array`, where `T` doesn't have a default constructor. – Dmitry Jul 14 '23 at 19:55
  • @Dmitry This will work even if if `T` doesn't have a default constructor - but it won't work if `T` isn't copyable. – Ted Lyngmo Jul 15 '23 at 04:16