C++中按返回类型重载

2020-10-12 21:43:22

//这是OK std::string to_string(Int I);std::string to_string(Bool B);std::string si=to_string(0);std::string sb=to_string(True);//这不是OK int from_string(std::string_view s);bool from_string(std::string_view s);int i=from_string(";7";);bool b=from_string(";false";);

按参数类型重载是许多命令性语言的一个非常简单的功能。然而,它们中的大多数都不支持按返回类型重载。尤其是C++不支持。例如,clang抱怨:

<;source>;:4:6:错误:不能重载仅返回类型不同的函数bool from_string(std::string_view s);~^<;source>;:3:5:注意:此处为上一个声明int from_string(std::string_view s);~~^。

所以…。如果我告诉你,在C++中,我们实际上可以通过返回类型来重载,会怎么样?

转换运算符可以在C++中由用户定义。它们允许我们将自定义的隐式或显式转换添加到我们的类型中。这些转换本身也可以重载,这将我们引向一个简单的原型:

Struct to_string_t{std::string_view s;运算符int()const;//int from_string(std::string_view s);运算符bool()const;//bool from_string(std::string_view s);};int i=to_string_t{";7";};bool b=to_string_t{";true";};

查看Godbolt,这将编译并调用所需的转换运算符。这里需要注意的重要一点是,编译器需要知道转换的目标类型。因此,auto i=to_string_t{";7";};不能按预期工作。我的类型是to_string_t,而不是int。

虽然我们可以使用转换运算符技术在许多情况下通过返回类型实现重载,但它并不总是适用的。如前所述,编译器需要知道目标类型才能选择正确的转换运算符,除非强制执行,否则不会进行转换。我们已经看到了最简单的不应用转换的情况:

错误:对';bar';的调用是不明确的bar(from_string(";true";));^~~<;source>;:41:6:注意:候选函数void bar(Int);^<;source>;:42:6:注意:候选函数void bar(Bool);^。

类似地,这意味着std::cout<;<;from_string(";2";)<;<;std::Endl;不起作用。(这方面的错误消息有点可怕,因为我们至少有16个候选重载。)。

最后,只能隐式应用一个用户定义的转换,因此以下内容不起作用:

<;source>;:22:5:错误:调用';test_bar';test_bar(from_string(";3";));^~<;source>;:18:6:候选函数无效:对于1s t参数void test_bar(Bar B),没有已知的从';to_string_t';到';bar';的转换;^~<;源>;:22:5:错误:没有匹配的函数调用';test_bar';test_bar(from_string(";3";));^。

所有这些情况都可以通过向所需类型显式添加强制转换来解决,例如int(TO_STRING(";10";))。

C++中普通函数重载的一个重要方面是重载集的可扩展性。独立的作者和库只需在相同的命名空间中提供具有正确名称的函数,就可以添加到同一重载集中。我们也可以通过参数相关查找来添加到重载集中,尽管这是否应该被视为功能或错误稍有争议。

在其基本形式中,我们的转换运算符方法是不可扩展的。用户定义的转换函数必须是成员函数,并且我们不能在调用后将成员添加到其他类。因此,如果我们的库定义了。

Struct to_string_t{std::string_view s;运算符int()const;//int from_string(std::string_view s);运算符bool()const;//bool from_string(std::string_view s);};

那么这意味着“返回类型int和bool的重载”,其他库/文件不能添加到此。

有一种方法可以解决这个问题并增加可扩展性。这会增加一些实现复杂性,对于更专业的用例,实际上可能并不需要可扩展性。但是,我认为from_string在设计时应该考虑可扩展性。

注意:本节的其余部分更多地关注元编程和API设计,而不是返回类型重载。您可以跳到下一节查看最终版本。

这里的解决方案是转换函数可以模板化。我们将使用它将转换委托给模板专门化,然后模板专门化可以适当地扩展:

模板<;class T>;struct to_string_impl{static_assert(Always_false<;T>;,";转换为T不受支持";);};struct to_string_t{std::string_view s;template<;class T>;Operator T()const{return to_string_impl<;T>;:from_string(S);}};To_string_t from_string(std::string_view s){return to_string_t{s};}。

To_string_t中的转换现在已模板化,并始终调用to_string_impl<;T>;::from_string(S)。TO_STRING_IMPL<;T>;是专用于所有支持的转换的类模板。如果调用不支持的转换,Always_False<;T>;会生成一条漂亮的(-ish)错误消息。我们现在可以通过以下方式添加支持的转换:

同样,其他作者或最终用户也可以为自定义类型添加转换。有时,有条件地添加转换很有用。例如,只有当T本身支持from_string时,才可能支持my_range<;T>;。因此,习惯上向基本模板添加第二个模板参数:

部分专门化只有在T本身满足HAS_FROM_STRING<;T>;时才是“活动的”(当然,这是SFINAE的一个例子)。

模板<;class T>;auto impl_has_from_string(Int)->;decltype(to_string_impl<;T>;::from_string(std::decval<;std::string_view>;()),std::true_type{});template<;class T>;std::false_type impl_has_from_string(Char);template<;class T>;常量表达式bool has_from_string=dectype(impl_has_from_string<;T>;(0))::value;

这里,如果TO_STRING_IMPL<;T>;::FROM_STRING不存在,我们使用表达式SFINAE禁用第一个IMPL_HAS_FROM_STRING重载。Impl_has_from_string本身在int和char上被重载,并通过impl_has_from_string<;T>;(0)调用。这是一种“先尝试int重载,如果不适用,则接受char重载”的廉价方式。但是,如果我们尝试检查没有from_string的某些类型的has_from_string<;T>;,则会触发static_assert(Always_false<;T>;);,但是,如果我们尝试检查没有from_string的某些类型的has_from_string<;T>;,则会触发static_assert(Always_false<;T>;);因此,在基本模板中,我们将static_assert移动到to_string_t::Operator T()(请参见下一节)。

请注意,模板化的TO_STRING_IMPL类不是唯一的选项。我们还可以使用标记分派甚至正常重载,例如,通过将第二个参数重载的(用户可扩展的)void Convert_To(std::string_view s,T&;v)委托给它。

//基模板,专门化并提供静态from_string方法模板<;class=void>;struct to_string_impl{};Namespace Detail//隐藏Impl Detail{template<;class T>;auto has_from_string(Int)->;dectype(to_string_impl<;T>;::from_string(std::decval<;std::string_view>;(),std::true_type{});template<;类T>;std::false_type has_from_string(Char);}//检查T是否有from_string模板<;class T>;constexpr bool has_from_string=dectype(Detail::has_from_string<;T>;(0))::value;//返回类型重载机制struct to_string_t{std::string_view s;template<;class T>;运算符T()const{static_assert(has_from_string<;T>;,";转换为T不受支持";);return to_string_impl<;T>;::from_string(S);}};//提供";return-type重载函数";to_string_t from_string(std::string_view s){return to_string_t{s};}的便利包装。

模板<;>;struct to_string_impl<;int>;{static int from_string(std::string_view s);};template<;>;struct to_string_impl<;bool>;{static bool from_string(std::string_view s);};模板<;class T>;struct my_range{/*...*/};模板<;class T>;Struct to_string_impl<;my_range<;T>;,std::enable_if_t<;has_from_string<;T>;>;{static my_range<;t>;from_string(std::string_view s);};

HAS_FROM_STRING<;T>;可用于(在编译时)测试FROM_STRING是否可用于特定类型:

按参数类型重载在现代命令式语言中普遍存在,但通常不支持按返回类型重载。但是,我们可以使用用户定义的转换操作符(Mis)在C++中进行模拟。只要目标类型已知,就会选择适当的“重载”。基本版本很简单:

Struct to_string_t{std::string_view s;运算符int()const;//int from_string(std::string_view s);运算符bool()const;//bool from_string(std::string_view s);};to_string_t from_string(std::string_view s){return to_string_t{s};}

默认情况下,此解决方案不具有正常按参数类型重载的可扩展性。但是,我们可以通过委托给可以专门化的模板化类的模板化转换运算符来恢复它。在此过程中,我们还可以定义一个HAS_FROM_STRING<;T>;来帮助诊断或SFINAE。