Почему эта двусмысленность здесь?

У меня есть следующий минимальный код:

#include <boost/type_traits.hpp>

template<typename ptr_t>
struct TData
{
    typedef typename boost::remove_extent<ptr_t>::type value_type;
    ptr_t data;

    value_type & operator [] ( size_t id ) { return data[id]; }
    operator ptr_t & () { return data; }
};

int main( int argc, char ** argv )
{
    TData<float[100][100]> t;   
    t[1][1] = 5;
    return 0;
}

GNU С++ дает мне ошибку:

test.cpp: In function 'int main(int, char**)':
test.cpp:16: error: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for second:
test.cpp:9: note: candidate 1: typename boost::remove_extent<ptr_t>::type& TData<ptr_t>::operator[](size_t) [with ptr_t = float [100][100]]
test.cpp:16: note: candidate 2: operator[](float (*)[100], int) <built-in>

Мои вопросы:

  • Почему GNU С++ дает ошибку, но компилятор Intel С++ не является?
  • Почему изменение operator[] на следующие приводит к компиляции без ошибок?

    value_type & operator [] ( int id ) { return data[id]; }

Приветствуются ссылки на стандарт С++.


Как я вижу здесь два пути преобразования:

  • (1) int до size_t и (2) operator[](size_t).
  • (1) operator ptr_t&(), (2) int до size_t и (3) встроенный operator[](size_t).

Ответ 1

Это на самом деле довольно прямолинейно. Для t[1] разрешение перегрузки имеет следующие кандидаты:

Кандидат 1 (встроенный: 13.6/13) (T - некоторый произвольный тип объекта):

  • Список параметров: (T*, ptrdiff_t)

Кандидат 2 (ваш оператор)

  • Список параметров: (TData<float[100][100]>&, something unsigned)

Список аргументов задается 13.3.1.2/6:

Набор кандидатных функций для разрешения перегрузки - это объединение кандидатов-кандидатов, кандидатов, не являющихся членами, и встроенных кандидатов. Список аргументов содержит все операнды оператора.

  • Список аргументов: (TData<float[100][100]>, int)

Вы видите, что первый аргумент точно соответствует первому параметру кандидата 2. Но для первого параметра кандидата 1 требуется определенное пользователем преобразование. Таким образом, для первого параметра выигрывает второй кандидат.

Вы также видите, что результат второй позиции зависит. Давайте сделаем некоторые предположения и посмотрим, что получим:

  • ptrdiff_t int: первый кандидат выигрывает, потому что он имеет точное соответствие, а второй кандидат требует интегрального преобразования.
  • ptrdiff_t long: ни один из кандидатов не выигрывает, потому что оба требуют интегрального преобразования.

Теперь 13.3.3/1 говорит

Пусть ICSi (F) обозначает неявную последовательность преобразований, которая преобразует i-й аргумент в список в тип i-го параметра жизнеспособной функции F.

Жизнеспособная функция F1 определяется как лучшая функция, чем другая жизнеспособная функция F2, если для всех аргументов я ICSi (F1) не является худшей последовательностью преобразования, чем ICSi (F2), а затем... для некоторого аргумента j, ICSj (F1) является лучшей последовательностью преобразования, чем ICSj (F2), или, если не тот...

Для нашего первого предположения мы не получаем общего победителя, потому что кандидат 2 выигрывает первый параметр, а второй кандидат выигрывает за второй параметр. Я называю это criss-cross. Для нашего второго предположения кандидат 2 выигрывает в целом, потому что ни один параметр не имел худшего преобразования, но первый параметр имел лучшее преобразование.

Для первого предположения не имеет значения, что интегральное преобразование (int to unsigned) во втором параметре меньше зла, чем пользовательское преобразование другого кандидата в первом параметре. В перекрестке правила грубые.


Эта последняя точка может все еще запутать вас из-за всей суеты, так что давайте сделаем пример

void f(int, int) { }
void f(long, char) { }

int main() { f(0, 'a'); }

Это дает вам такое же запутанное предупреждение GCC (которое, я помню, на самом деле сбивало с толку меня, когда я впервые его получил несколько лет назад), потому что 0 преобразуется в long хуже, чем 'a' в int - но вы получаете двусмысленность, потому что вы находитесь в перекрестной ситуации.

Ответ 2

С выражением:

t[1][1] = 5;

Компилятор должен сфокусироваться на левой стороне, чтобы определить, что там происходит, поэтому = 5; игнорируется до тех пор, пока не будет разрешен lhs. Оставив нас с выражением: t[1][1], который представляет две операции, а второй - результат первого, поэтому компилятор должен учитывать только первую часть выражения: t[1]. Фактический тип (TData&)[(int)]

Вызов не соответствует точно каким-либо функциям, поскольку operator[] для TData определяется как принимающий аргумент size_t, поэтому для его использования компилятор должен будет преобразовать 1 из int до size_t с неявным преобразованием. Это первый выбор. Теперь еще один возможный путь - применить преобразование, определенное пользователем, для преобразования TData<float[100][100]> в float[100][100].

Преобразование int в size_t является интегральным преобразованием и оценивается как преобразование в таблице 9 стандарта, равно как и преобразованное пользователем преобразование из TData<float[100][100]> в float[100][100] в соответствии с §13.3.3.1. 2/4. Преобразование из float [100][100]& в float (*)[100] оценивается как точное соответствие в таблице 9. Компилятору не разрешается выбирать из этих двух последовательностей преобразования.

Q1. Не все компиляторы одинаково придерживаются стандарта. Общеизвестно, что в некоторых конкретных случаях компилятор будет работать иначе, чем другие. В этом случае разработчики g++ решили скулить по стандарту, не позволяя компилятору выбирать, в то время как разработчики Intel, вероятно, просто молча использовали свое предпочтительное преобразование.

Q2. Когда вы меняете подпись определенного пользователем operator[], аргумент соответствует точно переданному типу. t[1] идеально подходит для t.operator[](1) без каких-либо преобразований, поэтому компилятор должен следовать этому пути.

Ответ 3

Я попытался показать двух кандидатов для выражения t [1] [1]. Они равны RANK (CONVERSION). Отсюда двусмысленность

Я думаю, что уловка заключается в том, что встроенный оператор [] в соответствии с 13.6/13 определяется как

T& operator[](T*, ptrdiff_t);

В моей системе ptrdiff_t определяется как "int" (объясняет ли это поведение x64?)

template<typename ptr_t> 
struct TData 
{ 
    typedef typename boost::remove_extent<ptr_t>::type value_type; 
    ptr_t data; 

    value_type & operator [] ( size_t id ) { return data[id]; } 
    operator ptr_t & () { return data; } 
}; 

typedef float (&ATYPE) [100][100];

int main( int argc, char ** argv ) 
{ 
    TData<float[100][100]> t;    

    t[size_t(1)][size_t(1)] = 5; // note the cast. This works now. No ambiguity as operator[] is preferred over built-in operator

    t[1][1] = 5;                 // error, as per the logic given below for Candidate 1 and Candidate 2

    // Candidate 1 (CONVERSION rank)
    // User defined conversion from 'TData' to float array
    (t.operator[](1))[1] = 5;

    // Candidate 2 (CONVERSION rank)
    // User defined conversion from 'TData' to ATYPE
    (t.operator ATYPE())[1][1] = 6;

    return 0; 
}

ИЗМЕНИТЬ:

Вот что я думаю:

Для кандидата 1 (оператор []) последовательность преобразования S1 является Определенное пользователем преобразование - стандартное преобразование (int to size_t)

Для кандидата 2 последовательность преобразования S2 является Определенное пользователем преобразование → int в ptrdiff_t (для первого аргумента) → int to ptrdiff_t (для второго аргумента)

Последовательность преобразования S1 является подмножеством S2 и должна быть лучше. Но вот улов...

Здесь приведена приведенная ниже цитата из стандарта.

$13.3.3.2/3 - Стандарт последовательность преобразования S1 лучше последовательность преобразования, чем стандартная последовательность преобразования S2, если - S1 является правильная подпоследовательность S2 (сравнение последовательности преобразования в канонической формы, определенной в 13.3.3.1.1, исключая любую трансформацию Lvalue; последовательность преобразования идентичности считающейся подпоследовательностью любого последовательность преобразования не идентичности) или, если не тот...

$13.3.3.2 состояния - "Пользовательский последовательность преобразования U1 лучше последовательность преобразования, чем другая пользовательская последовательность преобразования U2, если они содержат один и тот же пользовательский функции преобразования или конструктора и если второе стандартное преобразование последовательность U1 лучше, чем вторая стандартная последовательность преобразования U2".

Здесь первая часть и условие "если они содержат одну и ту же определяемую пользователем функцию преобразования или конструктор" не выдерживают. Таким образом, даже если вторая часть и условие "если вторая стандартная последовательность преобразования U1 лучше второй стандартной последовательности преобразования U2". хорошо, ни S1, ни S2 не являются предпочтительными по сравнению с другими.

Вот почему gcc phantom сообщение об ошибке "ISO С++ говорит, что они неоднозначны, хотя худшее преобразование для первого лучше, чем худшее преобразование для второго"

Это объясняет двусмысленность тихого ИМХО

Ответ 4

Я не знаю, какой именно ответ, но...

Из-за этого оператора:

operator ptr_t & () { return data; }

существует уже встроенный оператор [] (подписка на массив), который принимает size_t как индекс. Таким образом, у нас есть два оператора [], встроенный и определенный вами. Бут принимает size_t, поэтому это считается незаконной перегрузкой.

//EDIT
это должно работать так, как вы планировали

template<typename ptr_t>
struct TData
{
    ptr_t data;
    operator ptr_t & () { return data; }
};

Ответ 5

Мне кажется, что с

t[1][1] = 5;

компилятор должен выбирать между.

value_type & operator [] ( size_t id ) { return data[id]; }

который соответствовал бы, если бы литерал int должен был быть преобразован в size_t или

operator ptr_t & () { return data; }

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


Что касается ошибки, кажется, что GCC как расширение компилятора хотел бы выбрать первую перегрузку для вас, и вы компилируете с флагом -pedantic и/или -Werror, который заставляет его придерживаться слова стандарта.

(Я не в педантическом настроении, поэтому никаких цитат из стандарта, особенно по этой теме.)

Ответ 6

Разрешение перегрузки - головная боль. Но так как вы наткнулись на исправление (исключить преобразование индексного операнда в operator[]), что слишком специфично для примера (литералы - это тип int, но большинство переменных, которые вы будете использовать, не являются), возможно, вы можете обобщить это:

template< typename IT>
typename boost::enable_if< typename boost::is_integral< IT >::type, value_type & >::type
operator [] ( IT id ) { return data[id]; }

К сожалению, я не могу проверить это, потому что GCC 4.2.1 и 4.5 принимают ваш пример без жалобы в --pedantic. Что действительно вызывает вопрос, является ли это ошибкой компилятора или нет.

Кроме того, как только я устранил зависимость Boost, она передала Комо.