How to resolve ambiguity in overloaded functions u

2019-03-27 03:45发布

I have an incredibly exciting library that can translate points: it should work with any point types

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 will work with points that have public x and y members, and it will also work with tuples/indexable containers where x and y are represented by the first and second element, respectively.

The problem is, another library defines a point class with public x and y, but also allows indexing:

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

};

My application, using both libraries, is the following:

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

but this makes GCC (and clang) unhappy:

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

Now I can see why this is happening, but I want to know how to fix this (assuming I can't change the internals of StupidPoint), and, if there is no easy workaround, how I might as a library implementer make this easier to deal with.

标签: c++ c++11 sfinae
4条回答
孤傲高冷的网名
2楼-- · 2019-03-27 04:10

If you want to give precedence to the case having public x/y, you can do this:

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

It goes without saying that the opposite configuration is given by switching the types of the first parameter.


If you have three or more options (says N), you can use a trick based on templates.
Here is the example above once switched to such a structure:

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

As you can see, now N can assume any value.

查看更多
一纸荒年 Trace。
3楼-- · 2019-03-27 04:11

You could provide an overload for StupidPoint:

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

live example


Another solution:

Since operator[] is const for StupidPoint, you can check this in your SFINAE condition:

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

live example


You could also use a different type-traits based approach to select the appropriate translate_point function:

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

live example

查看更多
唯我独甜
4楼-- · 2019-03-27 04:14

With SFINAE, I would do something like this:

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

By doing this, when T = (class with StupidPoint as base class), the second overload will be called.

But it is easier with a simple overload, as pointed out by m.s.

查看更多
Fickle 薄情
5楼-- · 2019-03-27 04:26

In this case, both of your overloads are under-constrained. You can't really call the second one with StupidPoint, but that isn't observable at the point of overload resolution. If you constrain both properly, you'll remove the ambiguity in this case:

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

Now, if operator[] returned an int& instead, this would still be ambiguous. In that case, you'd need a way to order the two overloads (maybe with an extra argument that is either int or ...?), or simply disallow that case. That's a separate design decision.

查看更多
登录 后发表回答