如何使用 SFINAE 解决重载函数中的歧义
How to resolve ambiguity in overloaded functions using 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
将与具有 public x
和 y
成员的点一起工作,它也将与 tuples/indexable 容器一起工作,其中 x
和y
分别表示第一个和第二个元素。
问题是,另一个库用publicx
和y
定义了一个点class,但也允许索引:
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 的内部结构),而且,如果没有简单的解决方法,我该如何作为图书馆实施者使这更容易处理。
您可以为 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;
}
您还可以使用不同的基于类型特征的方法来 select 适当的 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;
}
对于 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 = (class with StupidPoint
as base class) 时,将调用第二个重载。
但是正如 m.s 所指出的那样,使用简单的重载会更容易。
在这种情况下,您的两个重载都是欠约束的。您实际上不能用 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
或 ...
?),或者干脆不允许这种情况。这是一个单独的设计决定。
如果你想优先处理 public 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
可以取任何值。
我有一个非常令人兴奋的库,可以转换点:它应该适用于任何点类型
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
将与具有 public x
和 y
成员的点一起工作,它也将与 tuples/indexable 容器一起工作,其中 x
和y
分别表示第一个和第二个元素。
问题是,另一个库用publicx
和y
定义了一个点class,但也允许索引:
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 的内部结构),而且,如果没有简单的解决方法,我该如何作为图书馆实施者使这更容易处理。
您可以为 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;
}
您还可以使用不同的基于类型特征的方法来 select 适当的 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;
}
对于 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 = (class with StupidPoint
as base class) 时,将调用第二个重载。
但是正如 m.s 所指出的那样,使用简单的重载会更容易。
在这种情况下,您的两个重载都是欠约束的。您实际上不能用 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
或 ...
?),或者干脆不允许这种情况。这是一个单独的设计决定。
如果你想优先处理 public 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
可以取任何值。