4

I am currently working on a small mathematical vector class. I want two vectors class, Vector2 and Vector3 to be constructible from one to another. For example:

Vector2<float> vec2(18.5f, 32.1f); // x = 18.5; y = 32.1
Vector3<float> vec3(vec2);         // x = 18.5; y = 32.1; z = float()

To do so, and to ease extensibility, I would like to use a traits VectorTraits with its basic definition as so:

template <typename T>
struct VectorTraits
{
  typedef T                      VectorType;
  typedef typename T::ValueType  ValueType;

  static const unsigned int      dimension = T::dimension;
};

This form would allow an user to make a link between existing Vectors classes (like glm::vec2, for example) and my classes. It would then be possible to create a Vector2 from a glm::vec2.

Moreover, this technique could allow me to write a generic streaming operator for all classes defining a VectorTraits using SFINAE.

There is my problem, I haven't been able to define the operator<< so that is silently errors when VectorTraits is inapropriate for the given type.

Here is my last attempt (Ideone link here):

#include <iostream>
#include <type_traits>

// To define another operator
struct Dummy
{};

// Traits class
template <typename T>
struct VectorTraits
{
  typedef T                       VectorType;
  typedef typename T::ValueType   ValueType;
  static const std::uint16_t      dimension = T::dimension;
};

// Fake vector class. Defines the required typedef.
struct Vec
{
  typedef float   ValueType;
  static const std::uint16_t dimension = 2;
};

// Streaming operator for Dummy.
std::ostream& operator<<(std::ostream& stream, const Dummy& d)
{
    stream << "dummy.\n";
    return stream;
}

// Streaming operator attempt for classes defining VectorTraits.
template <class T, std::enable_if_t<(VectorTraits<T>::dimension > 0)>>
std::ostream& operator<<(std::ostream& stream, const T& vec)
{
    std::cout << "Traits. Dimension = " << VectorTraits<T>::dimension << "\n";
}

int main()
{
    std::cout << "Test\n";
    std::cout << Vec();
    std::cout << Dummy();
    return 0;
}

With this attempt, the error simply is

error: no match for 'operator<<' (operand types are 'std::ostream {aka std::basic_ostream<char>}' and 'Vec')
prog.cpp:33:15: note: candidate: template<class T, typename std::enable_if<(VectorTraits<T>::dimension > 0), void>::type <anonymous> > std::ostream& operator<<(std::ostream&, const T&)
 std::ostream& operator<<(std::ostream& stream, const T& vec)
               ^
prog.cpp:33:15: note:   template argument deduction/substitution failed:
prog.cpp:41:19: note:   couldn't deduce template parameter '<anonymous>'

If I change

template <class T, std::enable_if_t<(VectorTraits<T>::dimension > 0)>>

to

template <class T, std::enable_if_t<(VectorTraits<T>::dimension > 0)>* = 0>

I get another error

prog.cpp:13:35: error: 'char [21]' is not a class, struct, or union type
   typedef typename T::ValueType   ValueType;

The only version I managed to get working was with an empty VectorTraits class which had to be specialized for each Vector. But I also wanted to provide a way to "automatically" be a Vector with some typedefs defined.

I don't understand why, in the show version, my operator doesn't get retained by the compiler. I also tried some variants but it always either matches everything or nothing.

Telokis
  • 3,399
  • 14
  • 36
  • There's a difference between whether you want a trait that only picks out *your* vector (in which case you have an empty primary template and specialize for your vector), or whether you want to detect *any* vector-like type. In the latter case, you'll need a generic existence-checking trait that checks whether a type has a member of certain kind etc. – Kerrek SB Oct 15 '15 at 08:32
  • In my case, I really want to be able to detest any `Vector`-like type. But I have not been able to understand how to achieve this properly. More than the solution, I really want to understand what I am missing here. – Telokis Oct 15 '15 at 08:34
  • It would help if you specify the actual problem with that implementation (post the actual error message or whatever that your are getting). As it is now written, it is just "does not work". – Petr Oct 15 '15 at 08:34
  • (Slightly tangentially, the [pretty printer code](http://stackoverflow.com/q/4850473) contains a trait to detect "anything that looks like a container". Maybe that gives you some feeling of the kind of TMP used for such selection mechanisms.) – Kerrek SB Oct 15 '15 at 08:38
  • @Petr I forgot to do so. I have updated the question with more informations. – Telokis Oct 15 '15 at 08:39
  • @KerrekSB I am going to take a look at this link, thank you. – Telokis Oct 15 '15 at 08:40
  • For enable SFINAE, you have to do something like `typename = std::enable_if_t<..>`. But then you got other errors [Demo](http://coliru.stacked-crooked.com/a/5263d2c98910ff83) – Jarod42 Oct 15 '15 at 08:40
  • @Jarod42 Yes, thanks. I updated the example above by changing this part to `std::enable_if_t<...>* = 0` which should also work for SFINAE. (I get the same error as in your example) – Telokis Oct 15 '15 at 08:42

4 Answers4

4

One issue is that you don't supply a default argument to the result of your std::enable_if_t instantiation, so template argument deduction fails. One way to fix this is to add * = nullptr to it:

template <class T, std::enable_if_t<(VectorTraits<T>::dimension > 0)>* = nullptr>
std::ostream& operator<<(std::ostream& stream, const T& vec)

However, now we get an error because inside the VectorTraits<T> instantiation, T::ValueType is required. This is not in an SFINAE context, so a hard fail will occur if that member does not exist. We can fix this by adding an SFINAE check to it in our template parameters:

template <class T, typename = typename T::ValueType, 
          std::enable_if_t<(VectorTraits<T>::dimension > 0)>* = nullptr>
std::ostream& operator<<(std::ostream& stream, const T& vec)

You could factor this out into an external IsValidVector check so that you have a single point to update if you require these kinds of checks a number of times.

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • What is not clear for me is why in the OP's original code, only the `< – Petr Oct 15 '15 at 08:51
  • Thanks for you answer. I noticed the mistake you pointed in your first part and edited it out as a request. However, your second solution doesn't fit my expectations since it would require `T` to define a type `ValueType`. This would prevent the user from defining `VectorTraits` by hand. – Telokis Oct 15 '15 at 08:53
  • @Petr I manually defined the `operator<<` for `Dummy` so it will always be a better match than the buggy one. – Telokis Oct 15 '15 at 08:54
  • 1
    @Petr: there is `std::ostream& operator<<(std::ostream& stream, const Dummy& d)` whereas there is a conversion from `const char[N]` to `const char*`. – Jarod42 Oct 15 '15 at 08:54
  • @Jarod42, but why does this fail then in the modified code with `=nullptr`? – Petr Oct 15 '15 at 08:59
  • 1
    @Ninetainedo ah, I see. In that case, you're probably best going with Jarod42's solution, as you could fully-specialize `VecTraits_Impl`. – TartanLlama Oct 15 '15 at 09:03
  • @Petr Tha'ts exactly what I'd like to understand. I don't understand why the overload gets considered even though the declaration is not valid. – Telokis Oct 15 '15 at 09:04
  • @Ninetainedo because there is no `operator<<` for `std::ostream` and `const char[N]`, so other overloads are considered. – TartanLlama Oct 15 '15 at 09:08
  • @TartanLlama, why then is not this a hard fail in the OP's original code? (We come back to my first comment in this thread). So I would expect that, both with `=nullptr` and without it, either the template is considered (and hard fails) for `char[N]` and for `Dummy`, either it is not considered at all (and does not fail). But it seems that it is considered with `=nullptr` and is not considered without it. – Petr Oct 15 '15 at 09:11
  • But when you look at the [example given by Jarod in a comment of the OP](http://coliru.stacked-crooked.com/a/5263d2c98910ff83), you can see that it tries my overload even for `Dummy` even if I defined the best overload possible for `Dummy`. That's what I don't understand. – Telokis Oct 15 '15 at 09:12
  • Honestly, I don't think I'm familiar enough with the myriad overload resolution rules and their interaction with template functions to answer conclusively. My feeling is that your overload is a *viable* overload (`[over.match.viable]`), so the template parameters will be checked as part of determining the best viable function (`[over.match.best]`). – TartanLlama Oct 15 '15 at 09:20
4

You may add a layer to your vectorTrait to be enable only when it is valid:

// Traits class
template <typename T, typename = void>
struct VectorTraits_impl
{
};


template <typename T>
struct VectorTraits_impl<T,
                        std::enable_if_t<std::is_integral<decltype(T::dimension)>::value>>
{
  typedef T                       VectorType;
  typedef typename T::ValueType   ValueType;
  static const std::uint16_t      dimension = T::dimension;
};

// Traits class
template <typename T>
using VectorTraits = VectorTraits_impl<T>;

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Thanks for your answer. But this one doesn't allow the user to specialize himself the `VectorTraits` to make my class "think" his particular type is a `Vector`. (Example [here](http://ideone.com/NbHKHS)) – Telokis Oct 15 '15 at 09:03
  • 1
    @Ninetainedo specialize `VectorTraits_impl` instead, like [this](http://ideone.com/LKM35b). – TartanLlama Oct 15 '15 at 09:05
  • But, when thinking again about it, I just have to swap `VectorTraits_impl` and `VectorTraits` so that the user can specialize `VectorTraits` while I use `VectorTraits_impl`. Well, I think that'll do it. Let me try it out and I'll come back to mark it as solved. – Telokis Oct 15 '15 at 09:05
  • @TartanLlama Yeah, thanks. I think I was too focused on `VectorTraits`. – Telokis Oct 15 '15 at 09:06
  • Ok, [that works exactly as expected](http://ideone.com/NbHKHS). Thank you very much for the answer. I always forget that, when working with templates, another level of indirection can solve any problem. – Telokis Oct 15 '15 at 09:14
  • 2
    If you have a problem with templates, add more templates. Now you have `std::integral_constant` problems. – TartanLlama Oct 15 '15 at 09:21
1

Your particular code does not work for the following reason.

For your vectors, std::enable_if_t<(VectorTraits<T>::dimension > 0)> is some type. Therefore, you declare operator<< as a template where the second parameter is a value of that type. For Dummy (and for char*) there is no such type, so SFINAE rules out this. But for Vec you have std::enable_if_t<(VectorTraits<T>::dimension > 0)> to be some type, and the compiler expects the template parameter to be some value of that type. Of course, it has no way to find out what that value should be.

Petr
  • 9,812
  • 1
  • 28
  • 52
  • Thanks for your answer. For `Dummy` and `char*`, there is a better match than my templated one so they never even consider my overload. For `std::enable_if_t<(VectorTraits::dimension > 0)>` being some type, that was a mistake when I rewrote the above code. I edited to add some more information right below the code snippet. – Telokis Oct 15 '15 at 08:56
0

The solution provided by Jarod42 didn't work on Visual Studio so I managed to find another one. I post it so it can help people who come accross this problem one day or another:

First, I declared the class VectorTraits without defining it as so:

template <typename T, typename = void>
struct VectorTraits;

The, I provided a specialization which is only enabled when T::dimension > 0:

template <typename T>
struct VectorTraits<T, std::enable_if_t<(T::dimension > 0)>>
{
  typedef T                       VectorType;
  typedef typename T::ValueType   ValueType;
  static const std::uint16_t      dimension = T::dimension;
};

And, finally, I check if the trait is available by using std::enable_if_t<(sizeof(VectorTraits<T>) > 0)>:

template <class T, typename = std::enable_if_t<(sizeof(VectorTraits<T>) > 0)>>
std::ostream& operator << (std::ostream& stream, const T& )
{
    return stream << "Traits. Dimension = " << VectorTraits<T>::dimension << "\n";
}

The final working example is available here or below:

#include <iostream>
#include <type_traits>

// Traits class
template <typename T, typename = void>
struct VectorTraits;

template <typename T>
struct VectorTraits<T, std::enable_if_t<(T::dimension > 0)>>
{
  typedef T                       VectorType;
  typedef typename T::ValueType   ValueType;
  static const std::uint16_t      dimension = T::dimension;
};


// Fake vector class. Defines the required typedef.
struct Vec
{
  typedef float   ValueType;
  static const std::uint16_t dimension = 2;
};

// Fake vector class. Defines the required typedef.
struct VecFake
{
};

template <>
struct VectorTraits<VecFake>
{
  typedef VecFake                       VectorType;
  typedef float   ValueType;
  static const std::uint16_t      dimension = 12;
};

// Streaming operator attempt for classes defining VectorTraits.
template <class T, typename = std::enable_if_t<(sizeof(VectorTraits<T>) > 0)>>
std::ostream& operator << (std::ostream& stream, const T& )
{
    return stream << "Traits. Dimension = " << VectorTraits<T>::dimension << "\n";
}

int main()
{
    std::cout << "Test\n";  // Test
    std::cout << Vec();     // Traits. Dimension = 2
    std::cout << VecFake(); // Traits. Dimension = 12
}
Telokis
  • 3,399
  • 14
  • 36