Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++14 constexpr union conditional initialization in constructor

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.

like image 653
cshelton Avatar asked Jun 03 '15 23:06

cshelton


People also ask

Can a constructor be constexpr?

A constexpr function must accept and return only literal types. A constexpr function can be recursive. It can't be virtual. A constructor can't be defined as constexpr when the enclosing class has any virtual base classes.

What does constexpr mean in C++?

The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile time constant expressions are allowed (provided that appropriate function arguments are given).

Can unions have constructors?

A union can have member functions (including constructors and destructors), but not virtual functions. A union cannot have base classes and cannot be used as a base class.

What is the difference between const and constexpr?

The principal difference between const and constexpr is the time when their initialization values are known (evaluated). While the values of const variables can be evaluated at both compile time and runtime, constexpr are always evaluated at compile time.


2 Answers

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.

like image 147
Richard Smith Avatar answered Oct 24 '22 22:10

Richard Smith


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.

like image 38
Potatoswatter Avatar answered Oct 24 '22 21:10

Potatoswatter