0

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)?

Camille
  • 455
  • 2
  • 4
  • 11

1 Answers1

3

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

Evg
  • 25,259
  • 5
  • 41
  • 83
  • Good point on the `typename` and `template` keywords, that was the main thing I was missing in my attempts. Unfortunately, does not compile on Clang due to full template specialization within class scope (https://godbolt.org/g/sSZaSA), though that can be worked around using a dummy template argument. MSVC just fails with `error C2770: invalid explicit template argument(s) for 'VertexTraits::TypeSelector::Type *VertexData::vertices(void)'` (whether using a type alias or not). – Camille Jul 18 '18 at 17:02
  • @Camille, I edited my answer, please take a look. I do not understand why MSVC fails to compile the first solution, probably it is a bug. The solution with `decltype` is accepted by MSVC and seems to be much prettier anyway. – Evg Jul 18 '18 at 18:48
  • MSVC can be a bit particular about completeness of template types, that might be part of the issue. The `decltype` approach is definitely more elegant! But trying to complete the implementation runs into a blind spot of the example -- how could the storage be defined and returned for it? The `vertices()` accessor needs to be specialized while the VertexData class remains unspecialized, which is not (AFAIK) allowed (https://godbolt.org/g/GdjLaQ). Resolving that would need specializing VertexData, which defeats the purpose of this approach... Maybe this isn't a feasible solution. :) – Camille Jul 18 '18 at 19:58
  • Actually, figured out a solution just as I posted this. Not everything has to be a template... A simple switch case can work. https://godbolt.org/g/6kVCLE Sometimes you can't see the forest for the trees! – Camille Jul 18 '18 at 20:02
  • Wait, that won't work if the vertex types are different. – Camille Jul 18 '18 at 20:14
  • @Camille, there can be a problem with `switch`: it is not `constexpr`. All branches should return the so to speak common types, even if they are not taken at runtime. If you had different types of streams in `VD`, your code won't compile. If you have access to a C++17 compiler, you can use a couple of `if constexpr` statements. Then you can return totally unrelated types. You can also use `auto` return type and let the compiler deduce the return type for you. And what's the point of `I >= 0` for `uint32_t I`? – Evg Jul 18 '18 at 20:23
  • This is still C++14, sadly -- edited tags to reflect this. As for I >= 0, just a reflexive habit of bounds checking array values... Whoops. – Camille Jul 18 '18 at 20:33
  • 1
    With C++14 you can use `auto` return type. Instead of `if constexpr` you can use function overloading: define `vertices_impl(std::integral_constant)` and `vertices_impl(std::integral_constant)` and then call it from `vertices()`: `return vertices_impl(std::integral_constant{})`. – Evg Jul 18 '18 at 20:42