C++ объркан между cast оператор и променлив конструктор

C++ (по-конкретно, реализацията на g++ на MinGW) се обърква. Имам математически векторен клас, който съдържа произволен номер от произволен тип елемент. Типът на елемента и броят на елементите се определят по време на компилиране.

Класът Vector се обърква между един от неговите конструктори и това, което нарекох оператор "преоразмеряване". Операторът за преоразмеряване позволява на програмиста да преобразува вектор с един размер във вектор с друг произволен размер. Ако кастинг векторът има повече елементи от базовия вектор, той се допълва с 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) изисква променлив конструктор, когато всъщност трябва да извиква оператора за преоразмеряване. Опитах се да ги направя изрични, но не се получи. Как да гарантирам, че C++ използва оператора за преоразмеряване, когато имам горния код, а не променливия конструктор?

Накратко, как да кажа на 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), той все още извиква конструктора vec3 с аргумент vec4. Не знам защо.

ДРУГА РЕДАКЦИЯ:

Правенето на конструктора изричен позволява неявните преобразувания да работят, но не и изричните. Което означава, че този код работи:

vec3 foo = someVec4;

Но този код все още ми дава грешка при статично твърдение:

vec3 foo = static_cast<vec3>(someVec4);

Което по принцип няма смисъл, защото декларирах variadic конструктора изричен и следователно той не трябва да се извиква там.

Освен това, при поискване, ето SSCCE

Версията TL;DR на това е, че моят код извиква явен конструктор, когато се опитвам изрично да извикам оператора за преобразуване на типове, но не и когато се опитвам да го извикам имплицитно.


person Publius    schedule 11.08.2012    source източник
comment
Вашите фрагменти обърнати ли са след Накратко, как да кажа на C++ да използва...?   -  person D.Shawley    schedule 11.08.2012
comment
Не съм сигурен какво питаш. Дефинирам и внедрявам конструктора преди оператора за преоразмеряване, ако това имате предвид.   -  person Publius    schedule 11.08.2012
comment
Потърсете Накратко, как да кажа на C++ да използва това: във вашия въпрос. Следва конструкторът. Останалата част от въпроса ви пита как да не използвате конструктора, а оператора за преобразуване.   -  person    schedule 11.08.2012
comment
о! разбирам го Добро обаждане. поправя това сега.   -  person Publius    schedule 11.08.2012
comment
Имате предвид 'Вектор‹T, N›::оператор(Аргументи ... аргументи);' вместо 'оператор Vector‹T, N›::Vector‹T, rN›();', нали? Предполага се, че vec3 и vec4 са typedefs?   -  person JRG    schedule 11.08.2012
comment
Да, позволете ми да изясня някои от тези неща.   -  person Publius    schedule 11.08.2012
comment
Можете ли да направите конструктора explicit?   -  person aschepler    schedule 11.08.2012
comment
Опитах се да направя различните неща ясни. Не проработи.   -  person Publius    schedule 11.08.2012
comment
Би било по-добре, ако публикувате пълен, компилируем пример и точното съобщение за грешка - вижте sscce.org   -  person JRG    schedule 11.08.2012


Отговори (4)


Мисля, че най-простото беше да накарате кода си да работи е да замените оператора за преобразуване с конструктор на преобразуване. Тъй като този конструктор е по-специализиран от променливия конструктор, той винаги трябва да има предимство.

person celtschk    schedule 11.08.2012
comment
Проблемът там е, че не може да провери броя на елементите, ако предоставя повече от един аргумент. - person Publius; 11.08.2012
comment
@Avi: Не разбирам. Вашият оператор за преобразуване има точно един аргумент, неявният this. Вашият конструктор на преобразуване естествено също ще има само един аргумент. Вашият променлив конструктор изобщо не трябва да се променя. - person celtschk; 11.08.2012
comment
Имам предвид, ако предоставя повече от един аргумент на конструктора. Искам да правя разлика между моментите, в които преоразмерявам вектора, и моментите, в които просто създавам такъв с правилния брой елементи. - person Publius; 11.08.2012
comment
Но вие не (и не можете) да предоставяте повече от един аргумент на конструктора, който замества вашия конструктор за преобразуване. За това е вашият (различен и непроменен) променлив конструктор. - person celtschk; 11.08.2012

Няма объркване. Конструкторът винаги ще бъде предпочитан пред функция за преобразуване и във вашия случай вашият тип е винаги конструируем от всякакъв вид аргумент. Ето много намален пример:

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.; няма да доведе до същия вид грешка, както преди. Компилаторът би се оплакал напр. че няма подходящо преобразуване от double в foo, сякаш конструкторът изобщо не съществува. Това е, което искаме, защото ако не съществува такъв конструктор, тогава правилата диктуват, че ще се търси подходящ оператор за преобразуване. (Е, не че е голяма помощ в случая с double, но няма значение.)

Обърнете внимание, че има няколко начина да използвате SFINAE и този се оказа любимата ми форма. Можете да намерите други, като научите за SFINAE. (И за протокола не е толкова страшно с правилното използване на шаблонни псевдоними, където в крайна сметка изглежда като EnableIf<std::is_same<T, int>>....)

person Luc Danton    schedule 11.08.2012
comment
Чувал съм за SFINAE. Проблемът е, че няма разлика между типовете, които векторният конструктор и каст операторът приемат, освен че конструкторът трябва да има толкова елементи, колкото се предполага, че съдържа конструираният вектор. Освен това, за клас Vector‹T, N›, всеки от тях приема аргументи Vector‹T, otherN›. Така че имам нужда от начин изрично да посоча дали използвам оператора за прехвърляне или конструктора. Освен това компилаторът ми все още не поддържа характеристики на типа :( - person Publius; 11.08.2012
comment
@Avi „конструкторът трябва да има толкова елементи, колкото се предполага, че съдържа конструираният вектор“ вероятно трябва да е условието за SFINAE. (И от въпроса ви вече проверявате за това в тялото.) Предоставят се оператори за преобразуване, за да „попълнят“ липсващо преобразуване, когато целевият тип не може да бъде променен, но изходният тип може. Следователно и двете са до голяма степен неразличими на ниво клиент. Ако наистина искате да правите разлика между преобразуване и (преобразуваща) конструкция, тогава дайте име на преобразуването, не го правете оператор. - person Luc Danton; 11.08.2012
comment
Не знам как да направя това без характеристики на типа. Освен това конструкторът не е конвертиращ конструктор. Не може да конвертира директно между вектори с различни размери. - person Publius; 11.08.2012
comment
@Avi Не мога да бъда по-конкретен за това как да използвам SFINAE без повече код. Също така имайте предвид, че „преобразуващ конструктор“ е термин от стандарта, който означава „не-explicit конструктор“. Не е свързано със семантиката на конструктора (т.е. какво всъщност прави). - person Luc Danton; 11.08.2012
comment
Все пак конструкторът е ясен. Определих го като изрично. Проблемът е, че когато се използва оператор за преобразуване, той извиква явен конструктор. Освен това публикувах SSCCE. - person Publius; 11.08.2012
comment
@Avi Сложих скоби в „(преобразуване) конструкция“ поради тази причина. Бих могъл да копирам и да поставя съответните правила, които управляват защо конструкторът се извиква (явно или не), но това ще ви помогне ли с вашия проблем? Отговорът ми (и тези коментари) са твърде дълги, каквито са. - person Luc Danton; 11.08.2012
comment
Вероятно, защото това, което прочетох за изричната ключова дума, предполага, че тя наистина не трябва да се извиква. Не виждам защо static_cast извиква конструктор. - person Publius; 11.08.2012
comment
Вижте 5.2.3, 5.2.4, 5.4 за значението на преобразуване/отливане на типа и 8.5 (по-специално параграф 16) за значението на директна конструкция. Днес нямам повече време. - person Luc Danton; 11.08.2012
comment
@Avi: explicit казва на компилатора да не използва конструктора за имплицитно преобразуване, а само за явни преобразувания. static_cast (и също C-style cast) е изрично преобразуване и следователно е позволено (и по този начин, предвид подходящ конструктор, задължително) да се използва изричният конструктор. - person celtschk; 11.08.2012

Направете своя конструктор ясен и използвайте:

vec4 someVec4; 
// ....
vec3 someVec3 = someVec4;
person JRG    schedule 11.08.2012
comment
Има няколко проблема с това. Първо, не използвам operator(), използвам оператор Vector‹T, rN›. Второ, това е проблем, дори ако имам израза vec3 someVec = (vec3) someVec4. И трето, имам различен конструктор, когато не са дадени аргументи. Благодаря все пак за отговора. - person Publius; 11.08.2012
comment
Ааа.. Опитайте static_cast‹vec3›(someVec4). Не използвайте отливки в стил c в c++, това е лоша форма. - person JRG; 11.08.2012
comment
По причини, които не мога да разбера, това не проработи. Предполагам, че проблемът е малко по-различен. Ще редактирам, за да отразя това. Освен това нямах представа, че static_cast работи за дефинирани от потребителя оператори за преобразуване като този. - person Publius; 11.08.2012
comment
Какво ще кажете за vec3 someVec3 = someVec4? - person JRG; 11.08.2012
comment
Това ми даде нова грешка! В него се казва, че преобразуването от Vector‹float, 4› към Vector‹float, 3› е двусмислено, защото не може да избира между оператора за прехвърляне и променливия конструктор. Но правенето на вариативния конструктор изрично му позволи да работи! Има ли начин изричните кастинги да работят? - person Publius; 11.08.2012

Разглеждайки вашия SSCCE, има някои стъпки за почистване, които можете да приложите.

Големият проблем с шаблона на универсален конструктор е, че той съвпада с всичко, освен ако конструктор без шаблон е точно съвпадение. Ако грешите дори с cv-квалификация, ще бъде избран шаблонът на универсалния конструктор. Когато имах подобен проблем, ми беше предложено да добавя стойност за маркиране като първи параметър:

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;
}
person CTMacUser    schedule 14.08.2012