什么时候应该使用 C + + 14自动返回类型演绎?

随着 GCC 4.8.0的发布,我们有了一个支持自动返回类型演绎的编译器,这是 C + + 14的一部分。有了 -std=c++1y,我可以这样做:

auto foo() { //deduced to be int
return 5;
}

我的问题是: 什么时候我应该使用这个特性? 什么时候它是必要的,什么时候它使代码更干净?

场景1

我能想到的第一种情况是,只要有可能。用这种方式编写的每个函数都应该是。这样做的问题是,它可能并不总是使代码更具可读性。

场景2

下一个场景是避免更复杂的返回类型:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
return t + u;
}

我认为这不会真的成为一个问题,尽管我猜在某些情况下返回类型明确地依赖于参数可能会更清楚。

场景3

其次,为了防止冗余:

auto foo() {
std::vector<std::map<std::pair<int, double>, int>> ret;
//fill ret in with stuff
return ret;
}

在 C + + 11中,我们有时可以只使用 return {5, 6, 7};来代替向量,但这并不总是有效,我们需要在函数头和函数体中指定类型。这纯粹是多余的,而且自动返回类型推断使我们避免了这种多余。

场景4

最后,它可以用来代替非常简单的功能:

auto position() {
return pos_;
}


auto area() {
return length_ * width_;
}

但是,有时候我们可能会查看函数,希望知道确切的类型,如果没有提供该类型,我们就必须转到代码中的另一个点,比如声明 pos_的位置。

结论

随着这些场景的展开,哪一个场景实际上证明了这个特性在使代码更清晰方面是有用的?那么我在这里没有提到的情景呢?在使用这个特性之前,我应该采取什么预防措施,以免它以后伤害到我?这个特性有没有带来什么新的东西,没有它就不可能实现?

请注意,这些多重问题是为了帮助寻找可以回答这个问题的视角。

65629 次浏览

没必要。至于什么时候应该,你会得到很多不同的答案。我会说,直到它实际上成为标准的一部分,并得到大多数主要编译器以同样的方式很好的支持之后,才会有所改变。

除此之外,这将是一场宗教争论。我个人认为,不输入实际的返回类型可以让代码更清晰,更容易维护(我可以查看一个函数的签名,知道它返回的是什么,而不必实际阅读代码) ,它消除了你认为它应该返回一种类型,而编译器认为另一种类型会导致问题的可能性(就像我使用过的所有脚本语言一样)。我认为自动化是一个巨大的错误,它会给数量级带来更多的痛苦而不是帮助。其他人会说你应该一直使用它,因为它符合他们的编程哲学。无论如何,这已经超出了这个网站的范围。

C + + 11提出了类似的问题: 什么时候在 lambdas 中使用返回类型推导,什么时候使用 auto变量。

对于 C 和 C + + 03中的问题,传统的回答是“跨语句边界我们使类型显式化,在表达式中它们通常是隐式的,但是我们可以通过强制转换使它们显式化”。C + + 11和 C + + 1y 引入了类型演绎工具,这样您就可以在新的地方省略该类型。

抱歉,但你不能通过制定一般规则来解决这个问题。你需要看一下特定的代码,然后自己决定是否它有助于指定所有地方的类型的可读性: 是让你的代码说,“这个东西的类型是 X”更好,还是让你的代码说,“这个东西的类型与理解这部分代码无关: 编译器需要知道,我们可能会解决它,但我们不需要在这里说”更好?

由于“可读性”不是客观定义的[ * ] ,而且它因读者而异,因此您有责任作为一段代码的作者/编辑,而这段代码不能完全由一个样式指南来满足。即使风格指南确实指明了规范,不同的人也会喜欢不同的规范,并且会倾向于发现任何不熟悉的东西“不太可读”。因此,一个特定的风格规则的可读性往往只能在其他风格规则的上下文中进行判断。

您的所有场景(即使是第一个场景)都将使用某人的编码风格。就我个人而言,我发现第二个用例是最引人注目的用例,但即便如此,我预计它将取决于您的文档工具。将函数模板的返回类型记录为 auto是没有什么帮助的,而将其记录为 decltype(t+u)则会创建一个可以(希望)依赖的已发布接口。

[ * ]偶尔有人试图做一些客观的测量。在一定程度上,任何人只要提出任何具有统计学意义和普遍适用性的结果,就会被在职程序员完全忽视,而相信作者的“可读性”本能。

一般来说,函数返回类型对于记录函数非常有帮助。用户将知道所期望的是什么。但是,有一种情况下,我认为最好去掉返回类型,以避免冗余。这里有一个例子:

template<typename F, typename Tuple, int... I>
auto
apply_(F&& f, Tuple&& args, int_seq<I...>) ->
decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
{
return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}


template<typename F, typename Tuple,
typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
auto
apply(F&& f, Tuple&& args) ->
decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
{
return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}

这个例子摘自官方委员会文件 N3493。函数 apply的作用是将 std::tuple的元素转发给函数并返回结果。int_seqmake_int_seq只是实现的一部分,可能只会让试图理解它的用户感到困惑。

如您所见,返回类型只不过是返回表达式的 decltype。此外,apply_并不意味着要被用户看到,当它与 apply的返回类型大致相同时,我不确定记录它的返回类型是否有用。我认为,在这种特殊情况下,删除返回类型可以使函数更具可读性。请注意,在将 apply添加到标准 N3915的建议中,这种返回类型实际上已经被 decltype(auto)所取代(同时请注意,我的原始回答早于本文) :

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}


template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

但是,在大多数情况下,最好保留该返回类型。在我上面描述的特殊情况下,返回类型是相当不可读的,潜在用户不会从了解它中获得任何东西。一个带有示例的好的文档会更加有用。


还有一件事没有被提及: 虽然 declype(t+u)允许使用 表达式 SFINAE,但是 decltype(auto)不允许(即使 有个提议改变了这种行为)。以 foobar函数为例,如果类型的 foo成员函数存在,它将调用该类型的 foo成员函数; 如果类型的 bar成员函数存在,它将调用该类型的 bar成员函数。假设一个类始终具有精确的 foobar,但两者都不同时存在:

struct X
{
void foo() const { std::cout << "foo\n"; }
};


struct Y
{
void bar() const { std::cout << "bar\n"; }
};


template<typename C>
auto foobar(const C& c) -> decltype(c.foo())
{
return c.foo();
}


template<typename C>
auto foobar(const C& c) -> decltype(c.bar())
{
return c.bar();
}

X的实例上调用 foobar将显示 foo,而在 Y的实例上调用 foobar将显示 bar。如果使用自动返回类型推导(不管有没有 decltype(auto)) ,都不会得到表达式 SFINAE,对 XY的实例调用 foobar将触发编译时错误。

它与函数的简单性没有任何关系(就像这个问题的 现在已经删除的副本假设的那样)。

返回类型是固定的(不要使用 auto) ,或者以一种复杂的方式依赖于模板参数(在大多数情况下使用 auto,当有多个返回点时与 decltype配对)。

考虑一个真实的生产环境: 许多函数和单元测试都依赖于 foo()的返回类型。现在假设由于某种原因需要更改返回类型。

如果所有地方的返回类型都是 auto,并且 foo()和相关函数的调用方在获取返回值时使用 auto,那么需要进行的更改非常少。否则,这可能意味着要进行数小时极其枯燥和容易出错的工作。

作为一个实际示例,我被要求将一个模块从到处使用原始指针更改为智能指针。修复单元测试比实际的代码更痛苦。

虽然还有其他方法可以处理这个问题,但使用 auto返回类型似乎是一个很好的选择。

我想提供一个返回类型 auto 非常完美的例子:

假设您想为一个长的后续函数调用创建一个短别名。使用 auto,你不需要关心原来的返回类型(也许将来会改变) ,用户可以点击原来的函数来得到真正的返回类型:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: 取决于 这个的问题。

对于场景3,我将使用要返回的局部变量来转换函数签名的返回类型。这将使客户端程序员更清楚地了解函数返回的内容。 像这样:

场景3 为了防止冗余:

std::vector<std::map<std::pair<int, double>, int>> foo() {
decltype(foo()) ret;
return ret;
}

是的,它没有 auto 关键字,但它的主体是相同的,以防止冗余,并给程序员谁没有访问源代码一个更容易的时间。