Как разрешить неоднозначность в перегруженных функциях с помощью SFINAE

У меня есть невероятно интересная библиотека, которая умеет переводить точки: она должна работать с любыми типами точек.

template<class T>
auto translate_point(T &p, int x, int y) -> decltype(p.x, p.y, void())
{
    p.x += x;
    p.y += y;
}

template<class T>
auto translate_point(T &p, int x, int y) -> decltype(p[0], void())
{
    p[0] += x;
    p[1] += y;
}

translate_point будет работать с точками, имеющими общедоступные члены x и y, а также с кортежами/индексируемыми контейнерами, где x и y представлены первым и вторым элементом соответственно.

Проблема в том, что другая библиотека определяет класс точек с общедоступными x и y, но также позволяет индексировать:

struct StupidPoint
{
    int x, y;

    int operator[](int i) const
    {
        if(i == 0) return x;
        else if(i == 1) return y;
        else throw "you're terrible";
    }

};

Мое приложение, использующее обе библиотеки, выглядит следующим образом:

int main(int argc, char **argv)
{
    StupidPoint stupid { 8, 3 };
    translate_point(stupid, 5, 2);
    return EXIT_SUCCESS;
}

но это делает GCC (и clang) несчастным:

error: call of overloaded ‘translate_point(StupidPoint&, int, int)’ is ambiguous

Теперь я понимаю, почему это происходит, но я хочу знать, как это исправить (при условии, что я не могу изменить внутренности StupidPoint), и, если нет простого обходного пути, как я, как разработчик библиотеки, могу упростить это. иметь дело с.


person jaymmer - Reinstate Monica    schedule 09.07.2016    source источник
comment
Какой из них вы хотите позвонить в этом случае?   -  person Mattia F.    schedule 09.07.2016
comment
В данном случае первая версия, т.к. она (по крайней мере теоретически) немного быстрее. Кроме того, operator[] также имеет неконстантную перегрузку, возвращающую int&, которую я пропустил в своем простом тестовом примере.   -  person jaymmer - Reinstate Monica    schedule 10.07.2016


Ответы (4)


Если вы хотите отдать приоритет случаю с общедоступными x/y, вы можете сделать это:

template<class T>
auto translate_point_impl(int, T &p, int x, int y) -> decltype(p.x, p.y, void())
{
    p.x += x;
    p.y += y;
}

template<class T>
auto translate_point_impl(char, T &p, int x, int y) -> decltype(p[0], void())
{
    p[0] += x;
    p[1] += y;
}

template<class T>
void translate_point(T &p, int x, int y) {
    translate_point_impl(0, p, x, y);
}

Само собой разумеется, что противоположная конфигурация задается переключением типов первого параметра.


Если у вас есть три или более вариантов (говорит N), вы можете использовать трюк, основанный на шаблонах.
Вот пример выше после перехода на такую ​​структуру:

template<std::size_t N>
struct choice: choice<N-1> {};

template<>
struct choice<0> {};

template<class T>
auto translate_point_impl(choice<1>, T &p, int x, int y) -> decltype(p.x, p.y, void()) {
    p.x += x; p.y += y;
}

template<class T>
auto translate_point_impl(choice<0>, T &p, int x, int y) -> decltype(p[0], void()) {
    p[0] += x;
    p[1] += y;
}

template<class T>
void translate_point(T &p, int x, int y) {
    // use choice<N> as first argument
    translate_point_impl(choice<1>{}, p, x, y);
}

Как видите, теперь N может принимать любое значение.

person skypjack    schedule 09.07.2016
comment
на самом деле это очень умно, но есть ли способ заставить это работать, если у вас есть 3 (или более) варианта на выбор? - person jaymmer - Reinstate Monica; 10.07.2016
comment
Я никогда не видел этого decltype(void()) раньше, это мило. Хотя мне это странно, здесь по умолчанию создается пустота? Приятно, что это работает. - person Chris Beck; 10.07.2016
comment
@jaymmer Да, вы можете использовать класс шаблона для устранения перегрузки. Вы хотите, чтобы я добавил пример к ответу? - person skypjack; 10.07.2016
comment
это было бы здорово, я не уверен, что понимаю, что вы имеете в виду - вы говорите о чем-то другом, чем то, что м.с. предложенный? - person jaymmer - Reinstate Monica; 10.07.2016
comment
@jaymmer Добавлен новый раздел в ответ. Дайте мне знать, если у вас есть сомнения. - person skypjack; 10.07.2016
comment
+1 за трюк с тегами. Это очень интересный способ сделать интуитивно понятный выбор разрешения перегрузки на основе ‹0› ‹1› и так далее. - person namezero; 14.03.2019

Вы можете предоставить перегрузку для StupidPoint:

auto translate_point(StupidPoint &p, int x, int y)
{
    p.x += x;
    p.y += y;
}

живой пример


Другое решение:

Поскольку operator[] является константой для StupidPoint, вы можете проверить это в своем условии SFINAE:

template<class T>
auto translate_point(T &p, int x, int y) -> decltype(p[0] += 0, void())
{
    p[0] += x;
    p[1] += y;
}

живой пример


Вы также можете использовать другой подход, основанный на признаках типа, для выбора соответствующей функции translate_point:

template<typename T, typename = void>
struct has_x_y : std::false_type { };

template<typename T>
struct has_x_y<T, decltype(std::declval<T>().x, std::declval<T>().y, void())> : std::true_type { };

template<typename T, typename = void>
struct has_index : std::false_type { };

template<typename T>
struct has_index<T, decltype(std::declval<T>().operator[](0), void())> : std::true_type { };

template<class T>
std::enable_if_t<has_x_y<T>::value> translate_point(T &p, int x, int y)
{
    p.x += x;
    p.y += y;
}

template<class T>
std::enable_if_t<!has_x_y<T>::value && has_index<T>::value> translate_point(T &p, int x, int y)
{
    p[0] += x;
    p[1] += y;
}

живой пример

person m.s.    schedule 09.07.2016

В этом случае обе ваши перегрузки недостаточно ограничены. Вы действительно не можете вызвать второй с помощью StupidPoint, но это не наблюдается в момент разрешения перегрузки. Если вы правильно ограничите оба, вы устраните двусмысленность в этом случае:

template<class T>
auto translate_point(T &p, int x, int y) -> decltype(p.x += x, p.y += y, void()) { ... };

template<class T>
auto translate_point(T &p, int x, int y) -> decltype(p[1] += y, void()) { ... } // prefer checking 1, so you don't allow an operator[] that takes a pointer

Теперь, если бы operator[] вместо этого вернул int&, это все равно было бы неоднозначно. В этом случае вам понадобится способ упорядочить две перегрузки (может быть, с дополнительным аргументом int или ...?) или просто запретить этот случай. Это отдельное конструкторское решение.

person Barry    schedule 09.07.2016

С SFINAE я бы сделал что-то вроде этого:

template<class T, bool>
auto translate_point(T &p, int x, int y) -> decltype(p[0], void())
{
    p[0] += x;
    p[1] += y;
}

template<class T, bool = std::is_base_of<StupidPoint, T>::value>
auto translate_point(T &p, int x, int y) -> decltype(p.x, p.y, void())
{
    p.x += x;
    p.y += y;
}

При этом, когда T = (класс с StupidPoint в качестве базового класса), будет вызвана вторая перегрузка.

Но проще с простой перегрузкой, как указал м.с.

person Mattia F.    schedule 09.07.2016