Is it possible to distinguish between braces and parentheses in a constructor?

Viewed 69

For certain reasons, I want to write a class A that behaves like this:

std::vector<int> x = A{1, 2, 3}; // [1,2,3]
std::vector<int> y = A{5}; // [5]
std::vector<int> z = A(5); // [0,0,0,0,0]

Most of this is simple (writing a conversion operator, etc), this is not what this question is about.

What I'm stuck at is, in A's constructor, detecting whether it was called using braces {...} or parentheses (...). I'm not sure if it's possible at all.

My best attempt is following: run on gcc.godbolt.org

#include <initializer_list>
#include <iostream>

template <bool Braces, typename ...P>
struct A
{
    template <typename ...Q>
    A(Q &&...)
    {
        std::cout << Braces << '\n';
    }
};

struct Sink
{
    template <typename T>
    Sink(T &&);
};

template <typename ...P>
A(P &&...) -> A<false, P...>;
A(std::initializer_list<Sink>) -> A<true>;

int main()
{
    A{1, 2, 3}; // 1  (except 0 in GCC)
    A(1, 2, 3); // 0
}

But there are two issues here:

  1. It doesn't work in GCC (Braces is always false).
  2. I no longer can deduce P... from braces (the deduction guide above always deduces it to an empty list). I need it to store a tuple of the constructor arguments, to later use them in the conversion operator. I want the conversion operator to work for any type, so I can't construct the vector directly in the constructor.

Is this possible?

1 Answers

I no longer can deduce P... from braces

That is a necessary function of your deduction guide. You type-erased all of those parameters in the guide. Which means, as far as the compiler is concerned, all of the arguments to the hypothetical function are of type Sink.

You have derived a contradiction. Your (imperfect) mechanism to detect list initialization requires using an initializer_list-based deduction guide. But that only works if all of the types are the same. You can achieve that by erasing the types. But if you erase the types, you erased them. They are not available to use as template parameters in the deduction guide.

They show back up in the actual constructor's parameters, but you need the template parameters to be in the type so that you can properly store them as members.

You cannot use a solution that erases types without suffering the effects of erasing those types. And you cannot have list initialization map to initializer_list<T> without having all of the types in the list be convertible to T. And since the only means of detecting the difference between list initialization and non-list initialization is to use an initializer_list<T> constructor... you've arrived at a contradiction.

The type of initialization of an object was never intended to be forwarded in C++. This is part of the reason why emplace-style overloads can't use list-initialization, or if they can, they have to use a second constructor that takes a homogeneous initializer_list<T>. They definitely cannot replicate the ability of list initialization to call non-initializer list constructors with braces.

Related