9

I would like to pick the union member initialized in the constructor based on an argument. The following is an example that works:

struct A {
    union {
        int i;
        float f;
    };
    A(double d, bool isint) {
        if (isint) new(&i) int(d);
        else new(&f) float(d);
    }
};

While I'm using int and float, the goal is to work with other more complex types (but still allowable in a C++14 union), hence the use of placement-new (and not an assignment).

The problem is that this constructor cannot be constexpr as placement-new is not allowed in constexpr methods. Is there any way around this (other than making the isint argument part of the formal type system)? Some type of conditional initalizer list would work, but I'm unaware of a way to do it.

timrau
  • 22,578
  • 4
  • 51
  • 64
cshelton
  • 360
  • 3
  • 8
  • 1
    IIRC, type punning is forbidden (in general) within constant expressions. You could, for example, store all members separately (`struct` instead of `union`) and use encapsulation to enforce only one of the members is active. – dyp Jun 03 '15 at 23:25
  • I guess it depends on exactly what type of type punning you are considering. I can certainly declare a constructor that initializes `i` and a different one that initializes `f`. Only one member of the union may be "active" at any point, but I'm not attempting to remove this. I'd be happy telling the compiler which member is active. – cshelton Jun 03 '15 at 23:32
  • 3
    You're right. This kind of usage of placement-new doesn't really circumvent the type system. -- It is possible to do this in an external function, e.g. `constexpr A make_A(double d, bool isint) { if(isint) return A(d, true_type{}); else return A(d, false_type{}); }`. It should be possible to call such a function from within the constructor, if you use non-anonymous unions instead. Something like `struct A { union storage { int i; float f; storage(double d, true_type); storage(double d, false_type); } m; A(double d, bool isint) : m(make_storage(d, isint)) {} };` – dyp Jun 03 '15 at 23:44
  • I tried something like this (with constructor forwarding it wasn't necessary to use a named union) and I don't see a way to convert from a (nominally) runtime bool to the type system. But maybe I'm missing something. – cshelton Jun 03 '15 at 23:48
  • Note that in your example of how to do this in an external function, you rely on the copy constructor. The copy constructor will have the same problem (imagine we augment the structure to include which member is initialized). So, I'm not sure this is doable with your external function example (except if the copy constructors of the underlying classes are trivial). – cshelton Jun 03 '15 at 23:59
  • Yes, I've just realized that. It is possible to implement copy via an external function, but we need at least to copy from the return value into the data member via a copy constructor :( – dyp Jun 04 '15 at 00:15
  • Kind of silly, but you can use such a `storage` as described above to some extent: http://coliru.stacked-crooked.com/a/0b264fb9da9706cc It isn't copyable though, and I doubt it can be made copyable. – dyp Jun 04 '15 at 00:24
  • Note that similarly you can’t have `struct A { int x, y; A(bool b); };` such that `A { true }` initializes `x` only but `A { false }` initializes `y` instead. That is, what bases or members are initialized and/or what initializer (at the syntactic level) is picked for initializing those bases or members cannot depend on the value of an argument. – Luc Danton Jun 04 '15 at 16:51
  • Luc, that's true. However, in the case of a struct, both members are initialized, just one (or the other) is initialized with the no-arg constructor in your example. In the case of a union, only one member is every initialized, so the question is whether I can select it at run-time. As my example points out, you can select at runtime (otherwise, you don't need a union!). However, I figure out how to do so in a constexpr constructor. – cshelton Jun 04 '15 at 19:47
  • dyp, that's interesting. Does it rely on RVO? The only way I can see to make the `storage` copyable is to add a dummy element to the union. In the copy-constructor, the dummy is initialized and then the body assigns `*this` the result of your storage factory. – cshelton Jun 04 '15 at 19:55
  • oops, of course, you cannot do operator= on `*this` in a constexpr function. Hmmm... I'm stuck. – cshelton Jun 04 '15 at 20:35
  • @cshelton: It does not rely on RVO. He is directly constructing the argument into the return value (that is why the return statement is `return { stuff };`). The code then binds the temporary returned by the function to an rvalue reference, which extends the lifetime of the temporary to that of the reference. There is guaranteed to be no copies / moves. RVO requires that the copy / move constructor be visible. – David Stone Jun 15 '15 at 21:18
  • @David, Thanks. I have successfully used this method when the types involved (int and double here) have trivial copy constructors. Then, the union has a defined copy constructor. However, when they do not have trivial copy constructors the union's copy constructor is deleted and clang and gcc complain (as they should) on the implicit copy in the construction of the union from the return value of the function. – cshelton Jun 15 '15 at 22:40

2 Answers2

8

There is a trick. The key pieces are:

  1. A defaulted copy or move constructor for a union copies the object representation (and thus copies the active member), and is permitted in constant expression evaluation.
  2. You cannot change the active union member in a constant expression evaluation after initialization is complete, but you can delegate to another constructor to delay the choice.
  3. If you delegate to the copy or move constructor, you can pass in another object that is already initialized to the right state and copy its active member.

Putting this together, we get:

template<typename T> struct tag {};
struct A {
  union {
    int i;
    float f;
  };
  constexpr A(tag<int>, double d) : i(d) {}
  constexpr A(tag<float>, double d) : f(d) {}
  constexpr A(double d, bool isint) : A(isint ? A(tag<int>(), d) : A(tag<float>(), d)) {}
};

constexpr A a(1.0, true); // ok, initializes 'i'
constexpr A b(5, false);  // ok, initializes 'f'

This is accepted by recent Clang, GCC, and EDG, and requires only C++11 constexpr.

Warning: GCC 5.1.0 had a bug where it miscompiled the above code (initializing a and b to 0); this bug is not present in earlier or later versions of GCC.

Richard Smith
  • 13,696
  • 56
  • 78
  • Does this work if the union's members are not trivial? I don't think it does. For instance, I should be able to do this with a class that does not have a copy constructor. Yet, I don't think this is possible. – cshelton Jul 31 '15 at 21:49
  • @cshelton Yes, this method is bulletproof. The member initializer `i(d)` is constructing the member "in place." – Potatoswatter Aug 01 '15 at 02:04
  • 1
    The dispatching constructor of `A` move-constructs the `A` instance from an `A` temporary; this technique requires that move (or copy) construction to be valid, which in turn requires all the union members of (array of) class type to have trivial move (or copy) constructors. I don't know of a way around that restriction. – Richard Smith Aug 04 '15 at 21:25
  • @Richard, I agree. constexpr unions are only possible when the union members are trivial. Yet, it would be nice to be able to write a single template class that can handle either case. This doesn't seem possible without new syntax for initialization. The solution I developed was messy but worked by splitting the two cases. – cshelton Aug 05 '15 at 18:49
1

For trivially constructible objects, there is no need for new. You can begin the object lifetime, and select the active union member, simply by the assignment operator.

struct A {
    union {
        int i;
        float f;
    };
    A(double d, bool isint) {
        if (isint) i = d;
        else f = d;
    }
};

If there's really a constructor somewhere inside the member, then it's necessary to use Richard's answer.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • 1
    To my reading, the question was asking for a `constexpr` constructor for `A`. You can't make the constructor `constexpr` here because it is not permitted to change the active member of a union during constant expression evaluation. – Richard Smith Aug 04 '15 at 21:26
  • @RichardSmith Yes, you're right. Clang and GCC both appear to have some rough edges and relax this rule somewhat. GCC allows the active member to be changed. (Both do reject this example if `constexpr` is added.) – Potatoswatter Aug 05 '15 at 03:59