c++ooptemplatesmember

Decide on a templated class whether it holds 2 variables or just 1


I want to have a nested class B that behaves as follows:

#include <iostream>
#include <type_traits>

template <typename T>
class A {

  struct B {
    size_t sz;
    
    if (T != void)
    T t;  // declare T field
  };

};

I have found this partial solution but it works for integral types only and has some inconveniences in accessing T data.

static constexpr bool is_void = std::is_same_v<void, T>;
using type = std::conditional_t<is_void, bool, T>;

struct B {
  size_t sz;
  type : (is_void ? 0 : sizeof(type));
  // if 'type' has a name, possible CE: Named bit-field has zero width
};

Alternatively, we can create something similar with the inheritance trick but I want to find other ways (no boilerplate code) the language provides (~specifically created for such cases).

struct empty_class {};
struct container { T t; };  // solves (std::is_class_v<T> && !std::is_final_v<T>)

static constexpr bool is_void = std::is_same_v<void, T>;
using base = std::conditional_t<is_void, empty_class, container>;

struct B : base {
  size_t sz;
};

Potentially, the specialization of A<void> can create a lot of boilerplate code.

Commentary

For clarity: I want universe std::__detail::__extent_storage that std::span has (it stores everything, except for void)


Solution

  • Even when a specialization of all of A isn't a viable option, you can specialize just the part that absolutely needs it. Here, the only difference between the two cases is whether T t exists or not.

    Approach A - Inheritance

    template <typename T>
    struct maybe {
      T t;
    };
    
    template <>
    struct maybe<void> { };
    
    template <typename T>
    class A {
      struct B : maybe<T> {
        std::size_t sz;
      };
    };
    

    Having a base class of type maybe<void> should not add anything to the size of B because of empty base class optimization.

    Approach B - std::conditional_t

    struct empty { };
    
    template <typename T>
    class A {
      struct B {
        std::size_t sz;
        [[no_unique_address]] std::conditional_t<std::is_same_v<T, void>, empty, T> t;
      };
    };
    

    This approach should also not cost any size increase if t is of type empty, however, it pollutes B with a possibly unwanted empty t member.

    Approach C - Specialize B

    template <typename T>
    class A {
      struct B {
        std::size_t sz;
        T t;
      };
    };
    
    template <>
    struct A<void>::B {
        std::size_t sz;
    };
    

    This approach wouldn't require specializing all of A, but all of B. It may be the most redundant in a larger example, but it is the cleanest and simplest.