Question

C++ (more specifically, MinGW's implementation of g++) is getting confused. I have a mathematical Vector class that contains an arbitrary number of an arbitrary type of element. The element type and number of elements are specified at compile time.

The Vector class is getting confused between one of it's constructors and what I have dubbed the "resize" operator. The resize operator allows the programmer to cast a vector of one size into a vector of another arbitrary size. If the cast vector has more elements than the base vector, it pads with 1's. Here's the implementation:

/*
 * resize operator:
 * T is the type of element the base vector holds
 * N is the number of elements the base vector holds
 * rN is the size of the new vector
 */
template<typename T, unsigned int N, unsigned int rN>
operator Vector<T, rN>() const 
{
    Vector<T, rN> resize;

    for (unsigned int i = 0; i < rN; i++)
    {
        resize[i] = i < N ? this->elements[i] : 1;
    }

    return resize;
}

The vector class also has a type-safe variadic constructor that can take any number of any combinations of elements (which must be of type T) and any number of Vectors (which can contain any number of elements and must be of type T) so long as the number of bare elements added to the number of elements in the supplied vectors is equal to the number of elements that the constructing vector contains.

Thus this would be valid:

vec3 foo(vec2(1, 2), 3);

but not this.

vec3 bar(vec4(1, 2, 3, 4), 5);

I ensure that the proper number of elements has been supplied at compile time by recursing through them all with a counter, then I use a static assertion to make sure the counter ends up at number of elements the vector can contain. This normally works out well, except for the following code:

vec4 bar(1, 2, 3, 4);
(vec3) bar; //PROBLEM HERE

What's happening is that the C++ thinks that (vec3) bar is asking for the variadic constructor when, in fact, it should be calling the resize operator. I've tried making them explicit, but it hasn't worked. How do I ensure that C++ uses the resize operator when I have the above code and not the variadic constructor?

In short, how do I tell C++ to use this:

//resize operator
template<typename T, unsigned int N, unsigned int rN>
Vector<T, N>::operator Vector<T, rN>();

instead of this:

//constructor
template<typename T, unsigned int N, typename ... Args>
Vector<T, N>::Vector(Args ... arguments);

when I have this code:

(vec3) someVec4;

In case it wasn't clear, vec3 and vec4 are defined as such:

typedef Vector<float, 3> vec3;
typedef Vector<float, 4> vec4;

EDIT:

News, everyone! Even when I use static_cast(someVec4) it still calls the vec3 constructor with a vec4 argument. I don't know why.

ANOTHER EDIT:

Making the constructor explicit allows implicit casts to work, but not explicit ones. Which is to say that this code works:

vec3 foo = someVec4;

But this code still gives me a static assertion failure:

vec3 foo = static_cast<vec3>(someVec4);

Which makes basically no sense because I declared the variadic constructor explicit, and thus it shouldn't be called there.

Also, upon request, here's an SSCCE

The TL;DR version of this is that my code is calling an explicit constructor when I try to explicitly call the typecasting operator, but not when I try to implicitly call it.

Was it helpful?

Solution

I think the simplest was to get your code working is to replace the conversion operator by a conversion constructor. Since that constructor is more specialized than the variadic constructor, it should always take precedence.

OTHER TIPS

There is no confusion. A constructor will always be preferred to a conversion function, and in your case your type is always constructible from any kind of argument. Here's a much reduced example:

struct foo {
    template<typename T>
    foo(T t);
}

template<typename T>
foo::foo(T)
{ static_assert( std::is_same<T, int>::value, "" ); }

Notice the declaration of the template constructor (I separated declaration from definition on purpose): by taking T, any kind of initializer is accepted. std::is_constructible<foo, T>::value holds for all T, even though only int will yield a correct program. Other types will trigger the static_assert when the constructor is instantiated.

There is a secret sauce to achieve what you want, and its name is SFINAE -- hopefully you've heard of it before. To explain loosely (in case you haven't), if you move a potential error from the body of the template to somewhere in the declaration then specializations that would yield such an error will be discarded in the process of overload resolution. To put it in code:

struct foo {
    template<
        typename T
        , typename std::enable_if<
            std::is_same<T, int>::value
            , int
        >::type...
     >
     foo(T t);
};

which would be the SFINAE version of the previous contrived example. With such a declaration then something like foo f = 42.; will not yield the same kind of error as before. The compiler would complain e.g. that there is no appropriate conversion from double to foo, as if the constructor didn't exist at all. Which is what we want because if no such constructor exists, then the rules dictate that an appropriate conversion operator will be searched for. (Well, not that it's a big help in the case of double but nevermind that.)

Note that there are several ways to make use of SFINAE and this one only happens to be my favourite form. You can find others by learning about SFINAE. (And for the record it isn't as scary with the proper use of template aliases, where it ends up looking like EnableIf<std::is_same<T, int>>....)

Make your constructor explict and use:

vec4 someVec4; 
// ....
vec3 someVec3 = someVec4;

Looking at your SSCCE, there are some clean-up steps you can apply.

The big problem with a universal constructor template is that it matches everything, unless a non-template constructor is an exact match. If you're wrong by even a cv-qualification, the universal constructor template will be chosen. When I had a similar problem, it was suggested to me to added a marking value as the first parameter:

enum my_marker { mark };
//...
template<typename T, unsigned int N>
class Vector
{
    //...
    template<typename ... Args>
    explicit Vector(my_marker, Args ... args);
};
//...
Vector<int, 4>  va( mark, a1, a2 );

Your other constructors won't use this marker, so now you can distinguish between them. By the way, you have another overlap with the constructors that can take a T value:

template<typename T, unsigned int N>
class Vector
{
    //...
    Vector( T empty );
    Vector( std::initializer_list<T> set );
    //...
};
//...
Vector<int, 4>  vb{ 5 };  // always chooses the list ctr
Vector<int, 4>  vc( 6 );  // I think this uses the single-entry ctr.

When you have an array as a function argument, it will be treated as a pointer by default, ignoring any sizing information. You must pass it by reference if you need to keep the size:

template<typename T, unsigned int N>
class Vector
{
    //...
    Vector( T const (&set)[N] );  // "T set[N]" -> "T *set"
    //...
};
//...
int             aa[ 4 ] = { 1, 2, 3, 4 }, bb[ 3 ] = { 5, 6, 7 };
Vector<int, 4>  vd( aa );  // The new signature won't accept bb.

This array-to-pointer conversion prevents arrays from being directly assignable, but they are implicitly assignable when computing the special functions. This means that your assignment operator is not needed; the default code will do the right thing.

Have you heard of iterators? If so, then using those plus delgating constructors, standard algorithms, and initializers can reduce your code.

#include <algorithm>
#include <cassert>
#include <initializer_list>

enum mark_t  { mark };

template< typename T, unsigned N >
class Vector
{
    // The "set" functions are unnecessary, see below.
public:
    // The automatically defined copy-ctr, move-ctr, copy-assign, and
    // move-assign are OK.

    T elements[N];

    Vector()  : elements{}  {}
    // Vector()  : Vector( T{} )  {}  // ALTERNATE
    // Can be removed if following constructor uses a default argument.

    Vector(T empty)
    // Vector(T empty = T{})  // ALTERNATE
    { std::fill( elements, elements + N, empty ); }

    Vector(T const (&set)[N])
    { std::copy( set, set + N, elements ); }

    Vector(std::initializer_list<T> set)
        : elements{}
    {
        assert( set.size() <= N );
        std::copy( set.begin(), set.end(), elements );
        // If you were willing to use std::for_each, why not use a more
        // appropriate algorithm directly?  The lambda was overkill.
        // WARNING: there's an inconsistency here compared to the cross-
        // version constructor.  That one fills unused spots with ones,
        // while this one does it with zeros.
        // WARNING: there's an inconsistency here compared to the single-
        // value constructor.  That one fills all elements with the same
        // value, while this one uses that value for the first element but
        // fills the remaining elements with zeros.
    }

    template<typename ... Args>
    explicit Vector( mark_t, Args ... args)
        : elements{ args... }
        //: elements{ static_cast<T>(args)... }  // ALTERNATE
    {}
    // Array members can now be directly initialized in the member part
    // of a constructor.  They can be defaulted or have each element
    // specified.  The latter makes the private "set" methods unnecessary.
    // The compiler will automatically issue errors if there are too
    // many elements for the array, or if at least one "Args" can't be
    // implicitly converted to "T", or if you have less than "N" elements
    // but "T" doesn't support default-initialization.  On my system, the
    // example "main" flags int-to-float conversions as narrowing and post
    // warnings; the alternate code using "static_cast" avoids this.

    template < unsigned R >
    explicit Vector( Vector<T, R> const &v )
        : Vector( static_cast<T>(1) )
    { std::copy( v.elements, v.elements + std::min(R, N), elements ); }

    T &operator [](unsigned int param)
    { return this->elements[param]; }
    const T &operator [](unsigned int param) const
    { return this->element[param]; }
};

typedef Vector<float, 2> vec2;
typedef Vector<float, 3> vec3;
typedef Vector<float, 4> vec4;

int main()
{
    vec4 someVec4(mark, 1, 2, 3, 4);
    vec3 foo = static_cast<vec3>(someVec4);

    return 0;
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top