0

How can I force a generic class to accept only:

  1. scalars, except pointers;
  2. and structures containing only members of type 1.
template < typename T >
class C
{
  static_assert(what_condition, "wrong argument");
  //...
};
zdf
  • 4,382
  • 3
  • 18
  • 29
  • 1
    I don't know about 2, but for 1, you have [type_traits](https://en.cppreference.com/w/cpp/header/type_traits). – PaulMcKenzie May 03 '23 at 22:19
  • 1
    @PaulMcKenzie I guess enumerating the members is impossible. – zdf May 03 '23 at 22:27
  • [C++ does not have reflection](https://stackoverflow.com/questions/17660095/iterating-over-a-struct-in-c), so it doesn't seem possible. – PaulMcKenzie May 03 '23 at 22:30
  • 1
    Although the code to do it is somewhat non-trivial, it actually is possible. Here's some [example code](https://stackoverflow.com/a/48430535/179910). – Jerry Coffin May 03 '23 at 23:02
  • @zdf Any feedback on my answer? Did you find it useful? – RandomBits May 18 '23 at 01:49
  • @RandomBits I appreciate your effort and upvoted it, but I was hoping for something simpler. I would probably use [this](https://akrzemi1.wordpress.com/2020/10/01/reflection-for-aggregates/). – zdf May 18 '23 at 15:23

2 Answers2

2

For item 1) (scalar and not a pointer) the following can be used:

static_assert(!std::is_pointer<T>() && 
               std::is_scalar<T>(), "wrong argument");

For item 2) (struct with pointer members), I don't believe it is possible, since C++ would have to support reflection to walk the struct's members at compile-time.

Probably the best you can do for structs is use std::is_trivially_copyable, but that would not detect members that are pointers.

PaulMcKenzie
  • 34,698
  • 4
  • 24
  • 45
  • I guess a solution would be to use tuples. But this contradicts one of the original goals: ease of use. If I use `tuple`, I will force the user to add a conversion method. For me, reflection is something that happens at runtime. At compile time, all the information is available, so I wouldn't be surprised if there was some sort of structure iterator. Maybe in a later version but before reflection. – zdf May 04 '23 at 06:33
  • 2
    @zdf: As I already noted in another comment, it can be done with a normal struct/class using [Boost magic_get](https://github.com/apolukhin/magic_get). – Jerry Coffin May 04 '23 at 06:35
  • @JerryCoffin Sadly, using Boost is out of question. – zdf May 04 '23 at 06:59
  • 1
    @zdf: So you look through the code and imitate the same basic idea. Also note that the library in question is independent of the rest of Boost. – Jerry Coffin May 04 '23 at 07:01
  • @JerryCoffin It turns out that I can count the members of the structure, using SFINAE, which in turn makes it possible to link the structure to a tuple, and then check the members of the tuple. But if all possible scenarios have to be considered, something very complicated results. – zdf May 04 '23 at 08:51
1

TLDR;

Use the PFR Library which is available either as part of Boost or standalone as header only. They use some very clever standards-compliant meta-programming to deduce the types in (possibly nested) structures as well as provide a tuple-like interface to such structures.

DIY

Since you are only asking to enforce type requirements, you can get by without all the machinery in the library which also supports runtime tuple-like access. The following is a bare-bones outline of how you might go about the task.

You can find the full code with build instructions on GitHub. The code works for the basic examples, but there are likely some bugs and other shortcomings that could be improved as the code is only an outline.

The final product of our development process will be the following template which will return true if T is a scalar but not a pointer or if T is a (possibly nested) structure with such members.

template<class T>
inline constexpr bool criteria_v;

Sample Structs

struct Foo {
    char a;
    int b;
    double c;
};

struct Bar {
    int *ptr;
};

Given our sample structs, we want to be able to write the following assertions.

static_assert(criteria_v<int>);
static_assert(not criteria_v<int*>);
static_assert(criteria_v<Foo>);
static_assert(not criteria_v<Bar>);

Testing Aggregate Initializers

The following family of overloads for the function constructible allows us to determine at compile-time if an aggregate initalization with a particular number of parameters is valid for our target type T.

struct universal_type {
    std::size_t ignore;
    template<class T>
    constexpr operator T& () const;
};

// `constructible` has three overloads which can be used to determine
// if T can be aggregate initlaized with a given number of arguments.

// Can we aggregate initialize T with no arguments?
template<class T, class U = decltype(T{})>
constexpr bool constructible(std::index_sequence<>) {
    return true;
};

// Can we aggregate initialize T with sizeof...(Ix) + 1 arguments?
template<class T, size_t I, size_t... Ix,
     class U = decltype(T{universal_type{I}, universal_type{Ix}...})>
constexpr bool constructible(std::index_sequence<I, Ix...>) {
    return true;
};

// If neither of the other overloads are choosen, then we must not be
// able to aggregate initialize T with sizeof...(Ix) arguments.
template<class T, size_t... Ix>
constexpr bool constructible(std::index_sequence<Ix...>) {
    return false;
};

We can test constructible with our sample struct Foo and see that aggregate initialization succeeds with at most three parameters (as expected since it has three members).

// Foo can be initlaized with 0, 1, 2, or 3 arguments.
static_assert(constructible<Foo>(std::index_sequence<>{}));
static_assert(constructible<Foo>(std::index_sequence<1>{}));
static_assert(constructible<Foo>(std::index_sequence<1, 2>{}));
static_assert(constructible<Foo>(std::index_sequence<1, 2, 3>{}));
static_assert(not constructible<Foo>(std::index_sequence<1, 2, 3, 4>{}));
static_assert(not constructible<Foo>(std::index_sequence<1, 2, 3, 4, 5>{}));

Number of Fields

We known that the maximum possible number of members for our target type T is sizeof(T) * CHAR_BIT in the instance that every field was a single bit. We can start with this maximum and recurse towards zero with the follwing struct to determine the maximum number of aggregate initializers T accepts and return that as the field count.

// Returns the number of members of T. Utilizes the contructible
// overloads as helpers.
template<class T>
struct aggr_field_count {
    template<size_t N>
    struct impl;

    template<size_t N> requires (N == 0)
    struct impl<N> { static constexpr size_t value = 0; };

    template<size_t N> requires (N > 0)
    struct impl<N> {
    static constexpr bool good = constructible<T>(std::make_index_sequence<N>{});
    static constexpr size_t value = good ? N : impl<N - 1>::value;
    };

    static constexpr size_t value = impl<sizeof(T)>::value;
};

template<class T>
inline constexpr auto aggr_field_count_v = aggr_field_count<T>::value;

We can assert that Foo has three fields and Bar one.

// Foo has 3 members and Bar has one member.
static_assert(aggr_field_count_v<Foo> == 3);
static_assert(aggr_field_count_v<Bar> == 1);

Field Types

We can extract the types as a tuple-type using structured binding that are not actually materialized. I have only included the specializations for up to 3 members in the struct. This is the only part of the algorithm that is limited by the code because you have to manually write the structured bindings as far as I can tell (i.e. there is no meta-programming trick to make it work for arbitrary N). I suppose you could use shutter a macro, but that might be a heresy.

// Wrapper for containing field types.
template<class... Ts>
struct aggr_field_list {
    using type = std::tuple<Ts...>;
};

template<class T, size_t N>
struct aggr_field_type_impl;

template<class T>
struct aggr_field_type_impl<T, 0> {
    static auto ignore() { return aggr_field_list<>{};  }
    using type = decltype(ignore());
};

template<class T>
struct aggr_field_type_impl<T, 1> {
    static auto ignore() {
    T *x = nullptr; auto [a] = *x;
    return aggr_field_list<decltype(a)>{};
    }
    using type = decltype(ignore());
};

template<class T>
struct aggr_field_type_impl<T, 2> {
    static auto ignore() {
    T *x = nullptr; auto [a, b] = *x;
    return aggr_field_list<decltype(a), decltype(b)>{};
    }
    using type = decltype(ignore());
};

template<class T>
struct aggr_field_type_impl<T, 3> {
    static auto ignore() {
    T *x = nullptr; auto [a, b, c] = *x;
    return aggr_field_list<decltype(a), decltype(b), decltype(c)>{};
    }
    using type = decltype(ignore());
};

template<class T, size_t N = aggr_field_count_v<T>>
using aggr_field_types = typename aggr_field_type_impl<T, N>::type::type;

We can make the following assertions about Foo and Bar.

// Foo members should have types char, int, double.
using FooTypes = aggr_field_types<Foo>;
static_assert(std::is_same_v<std::tuple_element_t<0, FooTypes>, char>);
static_assert(std::is_same_v<std::tuple_element_t<1, FooTypes>, int>);
static_assert(std::is_same_v<std::tuple_element_t<2, FooTypes>, double>);

// Bar members should have type int*.
using BarTypes = aggr_field_types<Bar>;
static_assert(std::is_same_v<std::tuple_element_t<0, BarTypes>, int*>);

Applying Criteria

Finally, we can apply the criteria that are of interest, namely that we want to be able to idenity scalar types (except pointers) and (possibly nested) structures of such. Now that we have all the tools, this part is straight forward meta-programming.

template<class T>
struct criteria_impl;

template<class T> requires (not std::is_aggregate_v<T>)
struct criteria_impl<T> {
    static constexpr bool value = std::is_scalar_v<T> and not std::is_pointer_v<T>;
};

template<class T> requires (std::is_aggregate_v<T>)
struct criteria_impl<T> {
    using U = aggr_field_types<T>;
    static constexpr bool value = criteria_impl<U>::value;
};

template<class... Ts>
struct criteria_impl<std::tuple<Ts...>> {
    static constexpr bool value = (criteria_impl<Ts>::value and ...);
};

template<class T>
inline constexpr bool criteria_v = criteria_impl<T>::value;

And, after way too many preliminaries, we can make the relevant assertions.

static_assert(criteria_v<int>);
static_assert(not criteria_v<int*>);
static_assert(criteria_v<Foo>);
static_assert(not criteria_v<Bar>);

Yeah, I am surprised it is possible too.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
RandomBits
  • 4,194
  • 1
  • 17
  • 30