Search code examples
c++templatesc++17unionscrtp

Avoid writing constructor every union style class in C++17


I'm stuck with c++17 for a project, so I don't have access to designated initializers. I have a bunch of union types that I want to avoid initializing this way (because it is annoying):

MyUnionType x;
x.value = value_set_for_all_union_members;

I want to instead have this

MyUnionType x(value_set_for_all_union_members); 

But I also want to avoid writing an implementation for each union I create. I know that all my union types are going to be of the following structure, each union is meant to actually represent bit a bit field, so I actually do want type pruning here, I know it is "UB" according to C++, but that is on the C++ committee, in C it is not undefined behavior, and thus all compilers that I care about will do what I want here.

union Example{
    integer_type value;
    custom_safe_bitfield_abstraction<...> a;
    custom_safe_bitfield_abstraction<...> b;
    ...
};

I thought, okay, I'll just inherit the constructor, and use CRTP to extract the appropriate integer_type. Of course I can't inherit on a union directly, so instead I opted for this strategy:

struct Example : Base<Example>{
    union{
        integer_type value;
        custom_safe_bitfield_abstraction<...> a;
        custom_safe_bitfield_abstraction<...> b;
        ...
    };
};

using an anonymous union, I should be able to use it the same exact way as before (example.value should be the value inside of union).

Then in the implementation I do the following:

template<class Derived_T>
struct Base{
    using value_type = decltype(Derived_T::value); 
    explicit Base(value_type v){
        static_cast<Derived_T*>(this)->value = v; 
    }
}

This however doesn't work:

error: Incomplete type 'Example' used in nested name specifier
>    using value_type = decltype(Derived_T::value); 

Apparently we aren't allowed to refer to a member before it has been declared. Okay... but there must be some way to extract the type data out, after all I don't care about any memory alignment or anything.

The only other thing I can think of, is include the type in the CRTP template parameter (ie Base<Derived_T, value_type>) but I want to avoid doing that. I imagine there is some method for writing a function or specifying an internal type on each derived class, I don't want to do that either (and sort of defeats the purpose of what I'm doing anyway).

Is there a way to avoid writing the constructor per class, and with out sacrificing the other code duplication minimization goals I have?


Solution

  • Not exactly what you asked... but you can use the fact that you can use the type of D::value inside a member function... so using SFINAE over a template contructor...

    I mean, you can write something as

    template <typename D>
    struct Base
     {
       template <typename T>
       static constexpr bool is_value_type ()
        { return std::is_same_v<decltype(D::value), T>; }
    
       template <typename T, bool B = is_value_type<T>(),
                 std::enable_if_t<B, int> = 0>
       explicit Base (T v)
        { static_cast<D*>(this)->value = v; }
     };
    

    where the template constructor is enabled only if the deduced type of the argument is of the same type of B::value.

    Remember also to add the using

    using Base<Example>::Base;
    

    inside Example.

    The following is a full compiling example

    #include <type_traits>
    
    template <typename D>
    struct Base
     {
       template <typename T>
       static constexpr bool is_value_type ()
        { return std::is_same_v<decltype(D::value), T>; }
    
       template <typename T, bool B = is_value_type<T>(),
                 std::enable_if_t<B, int> = 0>
       explicit Base (T v)
        { static_cast<D*>(this)->value = v; }
     };
    
    struct Example : Base<Example>
     {
       using Base<Example>::Base;
    
       union
        {
          long value;
          long a;
          long b;
        };
     };
    
    int main ()
     {
       //Example  e0{0};   // compilation error
       Example  e1{1l};    // compile
       //Example  e2{2ll}; // compilation error
     }