С++ запутался между оператором приведения и вариативным конструктором

С++ (точнее, реализация g++ в MinGW) запутывается. У меня есть математический класс Vector, который содержит произвольное количество элементов произвольного типа. Тип элемента и количество элементов указываются во время компиляции.

Класс Vector путается между одним из своих конструкторов и тем, что я назвал оператором «изменения размера». Оператор изменения размера позволяет программисту преобразовать вектор одного размера в вектор другого произвольного размера. Если вектор приведения содержит больше элементов, чем базовый вектор, он дополняется единицами. Вот реализация:

/*
 * 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) и любое количество векторов (которые могут содержать любое количество элементов и должны быть типа 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 запрашивает вариативный конструктор, хотя на самом деле он должен вызывать оператор изменения размера. Я пытался сделать их явными, но это не сработало. Как убедиться, что 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);

Что в принципе не имеет смысла, потому что я объявил вариативный конструктор явным, и поэтому его не следует вызывать там.

Кроме того, по запросу вы можете получить 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
Ищите Короче говоря, как мне сказать С++ использовать это: в вашем вопросе. Далее следует конструктор. Остальная часть вашего вопроса спрашивает, как использовать не конструктор, а оператор преобразования.   -  person    schedule 11.08.2012
comment
Ой! Я понимаю. Хороший звонок. исправить это сейчас.   -  person Publius    schedule 11.08.2012
comment
Вы имеете в виду «Вектор‹T, N›::operator(Args ... arguments);' вместо «оператор 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) является явным преобразованием, и поэтому разрешено (и, следовательно, при наличии подходящего конструктора, необходимо) использовать явный конструктор. - person celtschk; 11.08.2012

Сделайте свой конструктор явным и используйте:

vec4 someVec4; 
// ....
vec3 someVec3 = someVec4;
person JRG    schedule 11.08.2012
comment
Есть несколько проблем с этим. Во-первых, я не использую оператор(), я использую оператор 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