Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why I can not use designated initalizers with structs that are not aggregates?

C++ has a nice new feature:

struct Point{
int x;
int y;
int z; 
};

Point p{.x=47, .y=1701, .z=0};

But if I add a constructor then I am forbidden from using the nice designated initalizers syntax:

struct Point{
Point(int x, int y, int z = 0): x(x), y(y), z(z){}
int x;
int y;
int z; 
};

static Point p{.x=47, .y=1701, .z = 0};

error: designated initializers cannot be used with a non-aggregate type 'Point'

Am I missing something obvious(why it would be terrible if designated initalizers worked with structs/classes that have public members, but are not aggregates) or this is just a missing feature that is just not added to the standard?

like image 477
NoSenseEtAl Avatar asked Dec 31 '22 19:12

NoSenseEtAl


2 Answers

Aggregate initialization (including initialization with designed initializers) circumvents the constructor of the class.

This is not a problem for aggregates, since they aren't allowed to have user-defined constructors. But if you allow this kind of initialization for classes with user-provided constructors (that do something useful), it can be harmful.

Consider this example:

class A
{
    static std::map<A *, int> &Indices()
    {
        static std::map<A *, int> ret;
        return ret;
    }

  public:
    int dummy = 0;

    A(int index)
    {
        Indices().emplace(this, index);
    }

    A(const A &) = delete;
    A &operator=(const A &) = delete;
    
    ~A()
    {
        auto it = Indices().find(this);
        std::cout << "Deleting #" << it->second << '\n';
        Indices().erase(it);
    }
};

If you were able to do A{.dummy = 42};, you'd get UB in the destructor, and would have no way to protect against this kind of usage.

like image 184
HolyBlackCat Avatar answered Jan 31 '23 14:01

HolyBlackCat


Designated initalizers where a feature lifted from C. Most C++ compilers are also C compilers, and it was a C feature first.

They added a restriction (that the initializers be in order) and applied it to C++ types that matched C types, and got it into C++. Most major C++ compilers already had it as a C++ extension (without the restriction); the restriction was checked with the compiler implementors as being reasonable, and then the "cost" of adding the feature was really low.

Once you have a constructor, it becomes a larger language issue. Does the initializer refer to the constructor arguments? If yes, we run into the problem that argument names are not unique. If no, then how do we handle it when the constructor sets a value and the initializer set a different value?

Basically we need function-arguments-by-name to get sensible designated initializers with constructors. And that is a new feature, not one simply lifted from C.

The workaround (for named arguments) is:

struct RawPoint{
  int x = 0;
  int y = 0;
  int z = 0;
};

struct Point {
 Point( int x_, int y_, int z_ = 0 ):
   x(x_), y(y_), z(z_)
 {}
 explicit Point( RawPoint pt ):
   Point( pt.x, pt.y, pt.z )
 {}
  int x, y, z;
};

then you can do:

Point pt( {.x=3} );

by accessing the RawPoint's designated initializer feature.

This is the same way you can have designated initializers in function calls.

This also works:

struct Point:RawPoint {
 Point( int x, int y, int z = 0 ):
   RawPoint{x,y,z}
 {}
 explicit Point( RawPoint pt ):
   RawPoint( pt )
 {}
};
like image 29
Yakk - Adam Nevraumont Avatar answered Jan 31 '23 16:01

Yakk - Adam Nevraumont