C++中强制类型转换运算符和可变参数构造函数混淆

7
C++(更确切地说,是MinGW的g++实现)出现了混淆。我有一个数学向量类,其中包含任意类型和数量的元素。元素类型和元素数量在编译时指定。
这个向量类在一个构造函数和一个所谓的“resize”运算符之间产生了混淆。resize运算符允许程序员将一个大小为n的向量转换成任意大小的向量。如果转换后的向量比基本向量拥有更多的元素,则用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;
}

向量类还具有类型安全的可变参数构造函数,可以接受任意数量和任意组合的元素(必须是类型T)和任意数量的向量(可以包含任意数量的元素,并且必须是类型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

简而言之,我的代码在尝试显式调用类型转换运算符时调用了一个显式构造函数,但在尝试隐式调用它时却没有。


我不确定你的问题是什么。如果您的意思是在resize运算符之前定义和实现构造函数,那是的,我是这样做的。 - Publius
1
请查看您的问题中的"In short, how do I tell C++ to use this:"。接下来是构造函数。您的其余问题是如何不使用构造函数,而是使用转换运算符。 - user743382
1
你的意思是使用 'Vector<T, N>::operator(Args ... arguments);' 而不是 'operator Vector<T, N>::Vector<T, rN>();',对吗?假设vec3和vec4是typedefs? - JRG
你能将构造函数设置为explicit吗? - aschepler
最好您发布一个完整的、可编译的示例和_确切的_错误消息——请参阅http://sscce.org/。 - JRG
显示剩余4条评论
4个回答

4

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

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

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

注意模板构造函数的声明(我特意将声明与定义分开):通过使用 T,可以接受任何类型的初始化。对于所有的 Tstd::is_constructible<foo, T>::value 都成立,即使只有 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>>...就不会很可怕了。)

我听说过SFINAE。问题是,除了构造函数必须具有与所构造的向量应包含的元素数量相同的元素之外,向量构造函数和转换操作符接受的类型之间没有区别。除此之外,对于一个类Vector<T, N>,它们都接受参数Vector<T, otherN>。因此,我需要一种明确指定我是在使用转换操作符还是构造函数的方法。此外,我的编译器尚不支持类型特征:( - Publius
“@Avi '构造函数必须具有与所构造的向量应包含的元素数量相同的元素'可能是SFINAE的条件。(从您的问题中可以看出,您已在正文中检查了这一点。)提供转换运算符以在目标类型无法修改但源类型可以修改时“填充”缺失的转换。因此,在客户端级别上两者基本上是不可区分的。如果您真的想区分转换和(转换)构造之间的区别,请给转换命名,而不要将其作为运算符。” - Luc Danton
我不知道如何在没有类型特征的情况下完成这个任务。此外,构造函数不是转换构造函数。它不能直接在不同大小的向量之间进行转换。 - Publius
@Avi,我在“(converting) construction”中加入了括号,原因是如此。我可以复制并粘贴相关规则,解释为什么构造函数被调用(显式或隐式),但这对你解决问题有帮助吗?我的回答(以及那些评论)已经够长了。 - Luc Danton
2
explicit 声明告诉编译器不要在 隐式 转换时使用构造函数,只有在显式转换时才使用。static_cast(以及 C 风格的转换)是一种显式转换,因此允许(并且如果给定适当的构造函数,则需要)使用 explicit 构造函数。 - celtschk
显示剩余4条评论

3

将构造函数声明为显式,并使用:

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

这个有几个问题。首先,我没有使用operator(),而是使用了operator Vector<T, rN>。其次,即使我有表达式vec3 someVec = (vec3) someVec4,也会有问题。第三,当没有参数时,我有一个不同的构造函数。虽然感谢您的答案。 - Publius
3
尝试使用static_cast<vec3>(someVec4)。在C++中不要使用C风格的转换,这是不好的风格。 - JRG
由于我无法理解的原因,那并没有起作用。我猜问题略有不同。我将进行编辑以反映这一点。此外,我不知道 static_cast 可以像那样用于用户定义的转换运算符。 - Publius
vec3 someVec3 = someVec4 怎么样? - JRG
那给了我一个新的错误!它说从Vector<float, 4>到Vector<float, 3>的转换是不明确的,因为它无法选择强制转换运算符和可变参数构造函数之间的区别。但是将可变参数构造函数显式化可以使其工作!有没有办法让显式转换起作用? - Publius

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.

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

你听说过迭代器吗?如果是这样,那么使用迭代器加上委托构造函数、标准算法和初始化程序可以减少你的代码。

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

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

问题在于如果我提供多个参数,它无法检查元素的数量。 - Publius
@Avi: 我不明白。你的转换运算符只有一个参数,即隐式的 this。你的转换构造函数自然也只会有一个参数。你的可变参数构造函数根本不需要改变。 - celtschk
如果我向构造函数提供多个参数,我希望能区分我调整向量大小的时间和我仅仅创建具有正确元素数量的向量的时间。 - Publius
但是您不能向替换您的转换构造函数的构造函数提供超过一个参数(也不应该这样做)。这就是您的(不同但未更改的)可变参数构造函数所用的方式。 - celtschk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接