有没有一种方法可以指定用户定义转换的优先级?

4
免责声明:我知道使用用户定义的隐式转换经常受到反对。但是,在我们的项目中,我们需要这些转换才能使各种模板类彼此良好地配合使用。
我需要定义用户定义转换的优先级,例如:
struct X{}
struct Y{}

struct Z{
    operator X(){...}
    operator Y(){...}
}

void foo(X x){...}
void foo(Y y){...}

// somewhere in some template client code
...{
    Z z = ...;
    ...
    foo(z); // WILL NOT COMPILE
}

这段代码无法编译,因为将类型为Z的变量转换成X或者Y并不明确。是否有方法可以解决这种不确定性呢?比如,能否告诉编译器:如果存在同时支持XY的函数重载,那么将Z强制转换成X而不是编译失败。
我知道没有简便的方法可以做到这一点。但是,也许可以通过一些模板魔术和我不知道的包装模板结构来实现。我可以更改客户端代码。不能使用显式转换,因为客户端代码是模板化的,不了解类型Z和可用的foo重载。

我可以通过重写 foo 来实现吗? - Yakk - Adam Nevraumont
决定你想调用哪种'foo'口味时的区别因素是什么? - John Dibling
@JohnDibling:如果存在这样的foo(并非总是如此),我总是更喜欢使用foo(X)而不是foo(Y)。如果只有一个foo(Y),那么我想要转换为Y。如果只有一个foo(X),那么当然要转换为X - gexicide
你唯一能做的就是修改 Z 吗?如果是这样,那么你就没什么运气了。如果我可以在其他点上做些事情,我就可以解决它。但需要从 gexicide 那里得到澄清。 - Yakk - Adam Nevraumont
@Yakk:不,我可以修改所有内容。但是使用foo(z)的代码可能不会硬编码转换(例如,不能简单地将static_cast转换为X),因为代码应独立于Z可以转换的类型。也就是说,调用foo(z)的代码既不能提到X也不能提到Y - gexicide
显示剩余4条评论
3个回答

1
你可以使用类似这样的模式来实现“优先调用”等功能:
struct P2 {};
struct P1: P2 {};

template<class A> 
void foo(A x, P1, typename std::common_type<X,A>::type* =nullptr) 
{ foo(static_cast<X>(x)); }

template<class A> 
void foo(A y, P2, typename std::common_type<Y,A>::type* =nullptr) 
{ foo(static_cast<Y>(y)); }

template<class A> void foo(A a) { foo(a,P1()); }

如果P2是P1的基础,并且使用P1进行调用,如果common_type可以编译,则使用第一版本。如果无法编译,则第一版本就像不存在一样(SFINAE),然后使用第二个版本。如果也无法编译...如果A只是X或只是Y,则调用相应的原始foo,否则由于类型不兼容而无法编译。

请注意,您甚至可以将“优先级”概括为

template<size_t N> struct P: P<N+1> {}
template<> struct P<10> {}

声明SFINAE函数,接受P<1>P<2>等,一直到P<10>,并将根调用放置在P<0>()中。


1
这是一个小系统,可以将变量智能转换为一系列类型Ts...,使得列表中第一个元素Ts...是隐式转换为变量所选择的类型:
namespace details {
  template<class...>struct types{using type=types;};
  template<class U, class Types, class=void>
  struct smart_cast_t:std::false_type {
    using type=U;
    template<class A>
    U operator()(A&& a)const{return std::forward<A>(a);}
  };
  template<class U, class T0, class...Ts>
  struct smart_cast_t<
    U, types<T0, Ts...>,
    typename std::enable_if<std::is_convertible<U, T0>::value>::type
  >:std::true_type
  {
    using type=T0;
    template<class A>
    T0 operator()(A&& a)const{return std::forward<A>(a);}
  };
  template<class U, class T0, class...Ts>
  struct smart_cast_t<
    U, types<T0, Ts...>,
    typename std::enable_if<!std::is_convertible<U, T0>::value>::type
  >:smart_cast_t< U, types<Ts...> >
  {};
}

template<class... Ts, class U>
auto smart_cast( U&& u )
-> decltype(details::smart_cast_t< U, details::types<Ts...> >{}( std::forward<U>(u) ))
{
  return details::smart_cast_t< U, details::types<Ts...> >{}( std::forward<U>(u) );
}

现在,我们可以按照以下方式修改foo的想法:
void foo_impl(X);
void foo_impl(Y);

template<class A>
void foo(A&& a) {
  foo_impl( smart_cast<X, Y>(std::forward<A>(a)) );
}

foo会将A尽可能地转换为X,如果不能转换则转换为Y

我们可以编写一个整个系统,通过在types< types<X,Y> >这样的包中传递foo的重载描述和一个重载集合,将其传递给一些神奇的代码,该代码将生成一个调度程序,但那是过度设计。

实时示例

这使用了C++11功能。在C++14中,我们可以在smart_cast中省略一些冗长的措辞。

设计非常简单。 我们创建一个types bundle以处理类型的打包(只是样板文件)。

然后我们的details::smart_cast_t具有回退基础专业化,仅将我们的U转换为U。 如果我们可以将U转换为第一个类型,则这样做。 否则,我们在父类型上进行递归,可能在基础专业化中终止。

我会隐藏细节,使我们的公共函数变得简单--它是smart_cast<type1, type2, type3, etc>(expression)。我们不必传递U表达式的类型,因为我们推断出来,然后将其传递到详细信息中进行处理。
在运行时,这将减少为单个隐式转换:所有上述内容都是在编译时完成的。
唯一的缺点是,这可能会导致某些编译器发出警告,因为我们使用了隐式转换。在smart_cast_t的第一个非基本特化中添加static_cast<T0>以避免这种情况。
我从true_typefalse_type无意中继承了smart_cast_t。这意味着smart_cast_t<U,types<Ts...>>::value的值将告诉您是否将U转换为任何Ts...,或者只是保留为U
调用方负责rvalue和lvalue类别。如果转换失败,则返回U而不是U&&(如果传递了rvalue)。我认为回退到U会生成更好的错误消息,但是如果您希望smart_cast<int, double>(std::string("hello"))无法编译而不是返回std::string,则只需从smart_cast_t的基本专业化中删除operator()即可。
顺便说一下,对于smart_cast<int, double>(""),我也不能正常工作。可能需要在那里添加一些typename std::decay<U>::type之类的东西。

0

是的,显式地进行类型转换:

foo(static_cast<Y>(z));

这是不可能的。客户端代码是一个模板代码,它不知道 foo 有哪些重载可用,也不知道 z 有哪些转换可用。这一切都由模板参数确定。我已经澄清了我的问题。 - gexicide

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接