Camille
Camille

Reputation: 485

Nested or inherited type traits

I'm looking to create a generic vertex data container with strong typing using templates. A partial interface would look like this:

template <VertexFormat VF>
class VertexData
{
public:
    template<uint32_t I>
    (StronglyTypedVertex*) vertices();
};

where VertexFormat is an enum, I is an index for different data streams, and StronglyTypedVertex is the resulting vertex data. Given vertex data stored as two separate streams of positions and texture coordinates (enum VertexFormat::Pos3_TexCoord2), using the above vertex data container would look like this:

VertexData<VertexFormat::Pos3_TexCoord2> vertexData;
Vector3* positions = vertexData.vertices<0>();
Vector2* texCoords = vertexData.vertices<1>();

This seems like the kind of thing that type traits would work for. I've managed to get something working using a flat type trait with 2 properties, like so:

template<VertexFormat VF, uint32_t I>
struct VertexTraits
{
};

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2, 0>
{
    using Type = Vector3;
};

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2, 1>
{
    using Type = Vector2;
};

and then, the signature of VertexData::vertices becomes:

template<uint32_t I>
VertexTraits<VF, I>::Type* vertices();

However, this isn't as convenient as I'd like because each permutation of vertex format and stream index requires its own type trait specialization. I was hoping to be able to do a single vertex trait with all streams in it, like so:

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2>
{
    using Stream0Type = Vector2; // Or some other similar declaration
    using Stream1Type = Vector3;
};

I've tried nesting trait types with a Stream trait inside the VertexTrait, and I've tried using inheritance through a CRTP, but I haven't been able to get the syntax quite right for either case. What approach would work for this? Can it be done in a way that would introduce a static assert or compile-time error if a stream is used that wasn't defined (i.e.: Stream2Type in the above example)?

Upvotes: 0

Views: 835

Answers (1)

Evg
Evg

Reputation: 26362

You can nest traits like this:

template<VertexFormat VF>
struct VertexTraits;

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2>
{
private:
    template<uint32_t I>
    struct TypeSelector;

public:
    template<uint32_t I>
    using Type = typename TypeSelector<I>::Type;
};

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2>::TypeSelector<0>
{
    using Type = Vector3;
};

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2>::TypeSelector<1>
{
    using Type = Vector2;
};

However, the syntax for using it is quite ugly:

template<uint32_t I>
typename VertexTraits<VF>::template Type<I>* vertices();

You can use a type alias

template<VertexFormat VF, uint32_t I>
using VertexTraits_ = typename VertexTraits<VF>::template Type<I>;

and then write

template<uint32_t I>
VertexTraits_<VF, I>* vertices();

https://godbolt.org/g/s82sPN

Do not forget to use typename and template for dependent types.

To avoid ugly looking specializations of TypeSelector outside VertexTraits, one can use decltype and overload resolution:

template<>
struct VertexTraits<VertexFormat::Pos3_TexCoords2>
{
    static Vector3 type_selector(std::integral_constant<uint32_t, 0>);
    static Vector2 type_selector(std::integral_constant<uint32_t, 1>);

    template<uint32_t I>
    using Type = decltype(type_selector(std::integral_constant<uint32_t, I>{}));
};

https://godbolt.org/g/VFRT9H

Upvotes: 3

Related Questions