解决 CRTP 函数重载歧义
Resolving CRTP function overload ambiguity
我有几个函数想用于 CRTP 基础 class 的派生 classes。问题是,如果我将派生的 classes 传递到用于 CRTP class 的自由函数中,就会出现歧义。一个最小的例子来说明这一点是这个代码:
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b)
{
std::cout << "LT, RT\n";
}
template<typename T, typename U>
void fn(const T a, const A<U>& b)
{
std::cout << "L, RT\n";
}
template<typename T, typename U>
void fn(const A<T>& a, const U& b)
{
std::cout << "LT, R\n";
}
int main()
{
C a; // if we change C to A<C> everything works fine
B b;
fn(a,a); // fails to compile due to ambiguous call
fn(b,a);
fn(a,b);
return 0;
}
理想情况下,我希望这适用于派生的 classes,就像我使用基础 class 一样(无需重新定义基础 class 的所有内容) es,CRTP 习语的全部要点是不必为多个 classes).
定义 fn
在这种情况下,可以很方便地创建可以部分特化来执行此操作的助手 class,并将函数转换为选择适当特化的包装器:
#include <iostream>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
struct fn_helper {
static void fn(const T &a, const U &b)
{
std::cout << "L, R\n";
}
};
template<typename T, typename U>
struct fn_helper<T, A<U>> {
static void fn(const T &a, const A<U> &b)
{
std::cout << "L, RT\n";
}
};
template<typename T, typename U>
struct fn_helper<A<T>, U> {
static void fn(const A<T> &a, const U &b)
{
std::cout << "LT, R\n";
}
};
template<typename T, typename U>
struct fn_helper<A<T>, A<U>> {
static void fn(const A<T> &a, const A<U> &b)
{
std::cout << "LT, RT\n";
}
};
template<typename T, typename U>
void fn(const T &a, const U &b)
{
fn_helper<T,U>::fn(a, b);
}
int main()
{
A<C> a;
B b;
fn(a,a);
fn(b,a);
fn(a,b);
return 0;
}
输出(gcc 9):
LT, RT
L, RT
LT, R
我希望现代 C++ 编译器只需要选择最适度的优化级别来完全优化包装函数调用。
鉴于在您的示例中,第二个函数的第一个元素和第三个函数的第二个元素不应继承自 CRTP,您可以尝试如下操作:
#include<iostream>
#include<type_traits>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b)
{
std::cout << "LT, RT\n";
}
template<typename U>
struct isNotCrtp{
static constexpr bool value = !std::is_base_of<A<U>, U>::value;
};
template<typename T, typename U, std::enable_if_t<isNotCrtp<T>::value, int> = 0>
void fn(const T a, const A<U>& b)
{
std::cout << "L, RT\n";
}
template<typename T, typename U, std::enable_if_t<isNotCrtp<U>::value, int> = 0>
void fn(const A<T>& a, const U& b)
{
std::cout << "LT, R\n";
}
int main()
{
C a;
B b;
fn(a,a);
fn(b,a);
fn(a,b);
return 0;
}
基本上我们在第一个和第二个参数中传递 CRTP 时禁用第二个和第三个函数,只留下第一个函数可用。
编辑:回答 OP 评论,如果 T
和 U
都继承第一个将被调用,这不是预期的行为吗?
使用以下代码:https://godbolt.org/z/ZA8hZz
编辑:更一般的答案,请参考用户 Barry
发布的答案
首先,您需要一个特征来查看某物是否类似于 A
。您不能在这里只使用 is_base_of
,因为您不知道 将从哪个 A
继承。我们需要使用额外的间接寻址:
template <typename T>
auto is_A_impl(A<T> const&) -> std::true_type;
auto is_A_impl(...) -> std::false_type;
template <typename T>
using is_A = decltype(is_A_impl(std::declval<T>()));
现在,我们可以使用这个特征来编写我们的三个重载:A
、仅左 A
和仅右 A
:
#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__), int> = 0
// both A
template <typename T, typename U, REQUIRES(is_A<T>() && is_A<U>())
void fn(T const&, U const&);
// left A
template <typename T, typename U, REQUIRES(is_A<T>() && !is_A<U>())
void fn(T const&, U const&);
// right A
template <typename T, typename U, REQUIRES(!is_A<T>() && is_A<U>())
void fn(T const&, U const&);
请注意,我这里只取T
和U
,我们不一定要沮丧和丢失信息。
C++20 中出现的概念的好处之一是编写它要容易得多。两者的特质,现在变成了一个概念:
template <typename T> void is_A_impl(A<T> const&);
template <typename T>
concept ALike = requires(T const& t) { is_A_impl(t); }
以及三个重载:
// both A
template <ALike T, ALike U>
void fn(T const&, U const&);
// left A
template <ALike T, typename U>
void fn(T const&, U const&);
// right A
template <typename T, ALike U>
void fn(T const&, U const&);
语言规则已经强制 "both A" 重载在可行时是首选。好东西。
我有几个函数想用于 CRTP 基础 class 的派生 classes。问题是,如果我将派生的 classes 传递到用于 CRTP class 的自由函数中,就会出现歧义。一个最小的例子来说明这一点是这个代码:
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b)
{
std::cout << "LT, RT\n";
}
template<typename T, typename U>
void fn(const T a, const A<U>& b)
{
std::cout << "L, RT\n";
}
template<typename T, typename U>
void fn(const A<T>& a, const U& b)
{
std::cout << "LT, R\n";
}
int main()
{
C a; // if we change C to A<C> everything works fine
B b;
fn(a,a); // fails to compile due to ambiguous call
fn(b,a);
fn(a,b);
return 0;
}
理想情况下,我希望这适用于派生的 classes,就像我使用基础 class 一样(无需重新定义基础 class 的所有内容) es,CRTP 习语的全部要点是不必为多个 classes).
定义 fn在这种情况下,可以很方便地创建可以部分特化来执行此操作的助手 class,并将函数转换为选择适当特化的包装器:
#include <iostream>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
struct fn_helper {
static void fn(const T &a, const U &b)
{
std::cout << "L, R\n";
}
};
template<typename T, typename U>
struct fn_helper<T, A<U>> {
static void fn(const T &a, const A<U> &b)
{
std::cout << "L, RT\n";
}
};
template<typename T, typename U>
struct fn_helper<A<T>, U> {
static void fn(const A<T> &a, const U &b)
{
std::cout << "LT, R\n";
}
};
template<typename T, typename U>
struct fn_helper<A<T>, A<U>> {
static void fn(const A<T> &a, const A<U> &b)
{
std::cout << "LT, RT\n";
}
};
template<typename T, typename U>
void fn(const T &a, const U &b)
{
fn_helper<T,U>::fn(a, b);
}
int main()
{
A<C> a;
B b;
fn(a,a);
fn(b,a);
fn(a,b);
return 0;
}
输出(gcc 9):
LT, RT
L, RT
LT, R
我希望现代 C++ 编译器只需要选择最适度的优化级别来完全优化包装函数调用。
鉴于在您的示例中,第二个函数的第一个元素和第三个函数的第二个元素不应继承自 CRTP,您可以尝试如下操作:
#include<iostream>
#include<type_traits>
template<typename T>
struct A{};
struct C : public A<C>{};
struct B{};
template<typename T, typename U>
void fn(const A<T>& a, const A<U>& b)
{
std::cout << "LT, RT\n";
}
template<typename U>
struct isNotCrtp{
static constexpr bool value = !std::is_base_of<A<U>, U>::value;
};
template<typename T, typename U, std::enable_if_t<isNotCrtp<T>::value, int> = 0>
void fn(const T a, const A<U>& b)
{
std::cout << "L, RT\n";
}
template<typename T, typename U, std::enable_if_t<isNotCrtp<U>::value, int> = 0>
void fn(const A<T>& a, const U& b)
{
std::cout << "LT, R\n";
}
int main()
{
C a;
B b;
fn(a,a);
fn(b,a);
fn(a,b);
return 0;
}
基本上我们在第一个和第二个参数中传递 CRTP 时禁用第二个和第三个函数,只留下第一个函数可用。
编辑:回答 OP 评论,如果 T
和 U
都继承第一个将被调用,这不是预期的行为吗?
使用以下代码:https://godbolt.org/z/ZA8hZz
编辑:更一般的答案,请参考用户 Barry
发布的答案首先,您需要一个特征来查看某物是否类似于 A
。您不能在这里只使用 is_base_of
,因为您不知道 将从哪个 A
继承。我们需要使用额外的间接寻址:
template <typename T>
auto is_A_impl(A<T> const&) -> std::true_type;
auto is_A_impl(...) -> std::false_type;
template <typename T>
using is_A = decltype(is_A_impl(std::declval<T>()));
现在,我们可以使用这个特征来编写我们的三个重载:A
、仅左 A
和仅右 A
:
#define REQUIRES(...) std::enable_if_t<(__VA_ARGS__), int> = 0
// both A
template <typename T, typename U, REQUIRES(is_A<T>() && is_A<U>())
void fn(T const&, U const&);
// left A
template <typename T, typename U, REQUIRES(is_A<T>() && !is_A<U>())
void fn(T const&, U const&);
// right A
template <typename T, typename U, REQUIRES(!is_A<T>() && is_A<U>())
void fn(T const&, U const&);
请注意,我这里只取T
和U
,我们不一定要沮丧和丢失信息。
C++20 中出现的概念的好处之一是编写它要容易得多。两者的特质,现在变成了一个概念:
template <typename T> void is_A_impl(A<T> const&);
template <typename T>
concept ALike = requires(T const& t) { is_A_impl(t); }
以及三个重载:
// both A
template <ALike T, ALike U>
void fn(T const&, U const&);
// left A
template <ALike T, typename U>
void fn(T const&, U const&);
// right A
template <typename T, ALike U>
void fn(T const&, U const&);
语言规则已经强制 "both A" 重载在可行时是首选。好东西。