C ++在强制转换操作符和可变参数构造函数之间混淆

时间:2012-08-11 11:21:55

标签: c++ c++11 variadic-templates typecast-operator

C ++(更具体地说,MinGW的g ++实现)变得混乱。我有一个数学Vector类,它包含任意数量的任意类型的元素。元素类型和元素数在编译时指定。

Vector类在其中一个构造函数和我称之为“resize”运算符之间变得混乱。调整大小运算符允许程序员将一个大小的向量转换为另一个任意大小的向量。如果投射向量具有比基本向量更多的元素,则用1填充。这是实施:

/*
 * 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;
}

vector类还有一个类型安全的可变参数构造函数,它可以接受任意数量的元素组合(必须是T类型)和任意数量的Vectors(可以包含任意数量的元素,并且必须是类型T)只要添加到提供的向量中的元素数量的裸元素的数量等于构造向量包含的元素的数量。

因此这是有效的:

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

但不是这个。

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

我确保在编译时通过使用计数器递归所有元素来提供适当数量的元素,然后我使用静态断言来确保计数器以向量可以包含的元素数量结束。这通常很有效,除了以下代码:

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

发生的事情是C ++认为(vec3)bar要求可变参数构造函数,实际上它应该调用resize运算符。我试过让它们明确,但它没有奏效。当我有上面的代码而不是可变参数构造函数时,如何确保C ++使用resize运算符?

简而言之,我如何告诉C ++使用它:

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

而不是:

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

当我有这段代码时:

(vec3) someVec4;

如果不清楚,vec3和vec4定义如下:

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

修改

新闻,大家好!即使我使用static_cast(someVec4),它仍然使用vec4参数调用vec3构造函数。我不知道为什么。

另一个编辑

使构造函数显式允许隐式强制转换工作,但不能显式。也就是说这段代码有效:

vec3 foo = someVec4;

但是这段代码仍然给我一个静态断言失败:

vec3 foo = static_cast<vec3>(someVec4);

这基本上没有意义,因为我声明了可变参数构造函数是显式的,因此不应该在那里调用它。

此外,根据要求,这是一个SSCCE

TL; DR版本是我的代码在我尝试显式调用类型转换操作符时调用显式构造函数,但是当我尝试隐式调用它时不是。

4 个答案:

答案 0 :(得分:4)

没有混淆。构造函数将始终优先于转换函数,在您的情况下,您的类型始终可以从任何类型的参数构造。这是一个非常简化的例子:

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

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

注意模板构造函数的声明(我故意将声明与定义分开):通过接受T,接受任何类型的初始化程序。 std::is_constructible<foo, T>::value适用于所有T,即使只有int会产生正确的程序。其他类型将在实例化构造函数时触发static_assert

实现你想要的东西有一个秘诀,它的名字是SFINAE - 希望你以前听过它。为了松散地解释(如果你没有),如果你将一个潜在的错误从模板的主体移动到声明中的某个地方,那么产生这种错误的特化将在重载解析的过程中被丢弃。把它放在代码中:

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

这将是之前人为设想的SFINAE版本。有了这样的声明,像foo f = 42.;之类的东西就不会像以前那样产生同样的错误。编译器会抱怨,例如没有从doublefoo的适当转换,就好像构造函数根本不存在一样。这是我们想要的,因为如果不存在这样的构造函数,则规则规定将搜索适当的转换运算符。 (嗯,不是说double是一个很大的帮助,但没关系。)

请注意,有几种方法可以使用SFINAE,而这只是我最喜欢的形式。您可以通过了解SFINAE找到其他人。 (并且对于记录而言,正确使用模板别名并不是那么可怕,它最终看起来像EnableIf<std::is_same<T, int>>...。)

答案 1 :(得分:3)

使您的构造函数显式并使用:

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

答案 2 :(得分:2)

查看您的SSCCE,您可以应用一些清理步骤。

通用构造函数模板的一个大问题是它匹配所有,除非非模板构造函数完全匹配。如果即使是cv-qualification也错了,也会选择通用构造函数模板。当我遇到类似问题时,建议我添加标记值作为第一个参数:

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

您的其他构造函数不会使用此标记,因此您现在可以区分它们。顺便说一下,你可以与构造函数有另一个重叠,可以采用T值:

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.

当您将数组作为函数参数时,默认情况下将其视为指针,忽略任何大小调整信息。如果您需要保持大小,则必须通过引用传递它:

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.

此数组到指针的转换可防止数组直接分配,但在计算特殊函数时它们是可隐式分配的。这意味着不需要您的赋值运算符;默认代码将做正确的事。

你听说过迭代器了吗?如果是这样,那么使用那些加上delgating构造函数,标准算法和初始值设定项可以减少代码。

#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;
}

答案 3 :(得分:1)

我认为最简单的方法是让代码工作,就是用转换构造函数替换转换运算符。由于该构造函数比可变参数构造函数更专业,因此它应始终优先。