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


144

随着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_声明的位置。

结论

在布置了这些方案之后,实际上哪个方案证明可以使用此功能来使代码更简洁?我在这里忽略提及的情况又如何呢?使用此功能之前应采取哪些预防措施,以免以后被我咬到?没有此功能,表格是否有新功能?

请注意,多个问题旨在帮助您找到可以回答这个问题的观点。


18
好问题!当您询问哪种情况使代码“更好”时,我还想知道哪种情况会使代码更糟
德鲁·多曼

2
@DrewDormann,那也是我想知道的。我喜欢利用新功能,但是知道何时使用这些功能以及何时不使用它们非常重要。在一段时间内,我们会花一些时间来解决新功能,因此,让我们现在就开始做,以便我们为正式发布做好准备:)
chris

2
@NicolBolas,也许吧,但是实际上它已经在编译器的实际发行版中了,这样的事实就足以使人们开始在个人代码中使用它了(在这一点上,它必须远离项目)。我是喜欢在自己的代码中使用最新功能的人之一,尽管我不知道该建议与委员会的配合程度,但我认为,这是该新选项中第一个包含的事实的东西。最好留待以后,或者(当我确定它即将到来时)((我不知道它会如何运作)恢复。
克里斯,

1
@NicolBolas,如果有帮助,它现在已被采用:p
chris

1
当前的答案似乎没有提到->decltype(t+u)用自动扣除替代会杀死SFINAE。
Marc Glisse 2014年

Answers:


62

C ++ 11提出了类似的问题:何时在lambda中使用返回类型推导,以及何时使用auto变量。

在C和C ++ 03中,对这个问题的传统答案是“在语句边界上,我们使类型显式,在表达式中,它们通常是隐式的,但我们可以使用强制类型转换使它们显式”。C ++ 11和C ++ 1y引入了类型推导工具,以便您可以在新地方省略类型。

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

由于“可读性”没有客观定义[*],而且它因读者而异,因此您有责任作为样式指南无法完全满足的一段代码的作者/编辑。即使在某种程度上,样式指南确实指定了规范,不同的人也会喜欢不同的规范,并且会倾向于发现不熟悉的东西,例如“可读性较差”。因此,通常只能在适当的其他样式规则的上下文中判断特定提议的样式规则的可读性。

您所有的场景(甚至第一个场景)都可以用于某人的编码风格。我个人认为第二个是最引人注目的用例,但是即使如此,我仍然希望它取决于您的文档工具。看到文档记录了函数模板的返回类型为,这不是很有帮助auto,而看到文档记录为decltype(t+u)创建了可以(希望)依赖的已发布接口。

[*]有时有人试图进行一些客观的测量。在某种程度上,任何人都无法得出任何具有统计意义的且普遍适用的结果,因此工作的程序员会完全忽略它们,而倾向于作者对“可读性”的直觉。


1
与lambda的连接很好(尽管这允许更复杂的功能体)。最主要的是,它是一个较新的功能,我仍在尝试平衡每个用例的利弊。为此,查看为什么将其用于什么用途很有帮助,这样我自己就可以发现为什么我更喜欢我的工作。我可能想得太多,但这就是我的方式。
克里斯

1
@chris:就像我说的,我认为这全都归结为同一件事。让您的代码说“这个东西的类型是X”会更好,还是让您的代码说“这个东西的类型无关紧要,编译器需要知道,我们可能会解决”但我们不必说“。当我们写的时候,1.0 + 27U我们断言后者,当我们写的时候,我们(double)1.0 + (double)27U断言前者。功能的简单性,重复程度,避免都decltype可能导致此问题,但是没有一个能够可靠地确定性。
史蒂夫·杰索普

1
让您的代码说“这个东西的类型是X”会更好,还是让您的代码说“这个东西的类型无关紧要,编译器需要知道,我们可能会解决”但我们不必说“。-这句话恰好符合我要寻找的内容。当我遇到使用此功能的选项时auto,通常都会考虑到这一点。
克里斯,

1
我想补充一点,IDE可能会减轻“可读性问题”。以Visual Studio为例:如果将鼠标悬停在auto关键字上,它将显示实际的返回类型。
andreee

1
@andreee:在限制范围内是正确的。如果一个类型有很多别名,那么知道实际的类型并不总是如您所愿。例如,迭代器类型可能是int*,但是实际上很重要(如果有的话)的原因是它的原因int*是因为这std::vector<int>::iterator_type是当前构建选项的原因!
Steve Jessop '18

30

一般来说,函数返回类型对记录函数有很大帮助。用户将知道预期的结果。但是,在一种情况下,我认为最好删除该返回类型以避免冗余。这是一个例子:

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是将a的元素转发std::tuple给函数并返回结果。在int_seqmake_int_seq仅仅是实现的一部分,并可能只会混淆试图了解它的任何用户。

如您所见,返回类型只不过decltype是返回表达式的a。而且,由于apply_不希望被用户看到,当它的返回类型与或多或少相同时,我不确定记录其返回类型是否有用apply。我认为,在这种特殊情况下,删除返回类型会使函数更具可读性。请注意,实际上decltype(auto)在建议添加apply到标准N3915的建议中实际上已经删除了此返回类型,并替换为(还请注意,我的原始答案早于本文):

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)允许使用表达式SFINAEdecltype(auto)但没有(即使更改此行为的建议)。例如,一个foobar将调用类型的foo成员函数(如果存在)或调用类型的bar成员函数(如果存在)的函数,并假设一个类始终具有完全相同性,foo或者bar一次都不具有:

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();
}

调用foobar上的实例X显示foo,同时呼吁foobar对的实例Y显示bar。如果改用自动返回类型推导(带有或不带有decltype(auto)),则不会获得表达式SFINAE并调用或foobar的实例,X否则Y将触发编译时错误。


8

没必要 至于何时应该-您将获得很多不同的答案。我要说的是,直到它实际上成为标准的一部分并得到大多数主要编译器的相同支持,它才可以。

除此之外,这将是一个宗教争论。我个人说过,从不输入实际的返回类型可以使代码更清晰,维护起来容易得多(我可以查看函数的签名,知道函数返回的内容,而不是实际必须阅读的代码),并且消除了以下可能性:您认为它应该返回一种类型,而编译器认为另一种类型会引起问题(就像我曾经使用过的每种脚本语言一样)。我认为汽车是一个巨大的错误,它将给人们带来的痛苦要比帮助更大。其他人会说您应该一直使用它,因为它符合他们的编程哲学。无论如何,这超出了该站点的范围。


1
我确实同意不同背景的人对此会有不同的看法,但是我希望这能得出C ++式的结论。在几乎每种情况下,滥用语言功能尝试将其转换为其他语言都是不可原谅的,例如使用#defines将C ++转换为VB。根据语言程序员的习惯,功能通常会在语言的心态中就正确使用和不正确使用达成一致。相同的功能可能会以多种语言显示,但是每种功能都有自己的使用指南。
克里斯

2
一些功能有很好的共识。许多人没有。我认识许多程序员,他们认为Boost的大部分都是垃圾,不应该使用。我也认识一些人,认为这对C ++来说是最重要的事情。无论哪种情况,我都认为应该进行一些有趣的讨论,但这确实是该站点上非建设性选择的确切例子。
Gabe Sechan

2
不,我完全不同意你的看法,我认为这auto是纯粹的祝福。它消除了很多冗余。有时重复返回类型简直是一种痛苦。如果您想返回一个lambda,那么即使不将结果存储到中也可能是不可能的std::function,这可能会产生一些开销。
吴永维2014年

1
在至少一种情况下,这几乎是完全必要的。假设您需要调用函数,然后在返回结果之前记录结果,并且这些函数不一定都具有相同的返回类型。如果通过将函数及其参数作为参数的日志记录函数执行此操作,则需要声明日志记录函数的返回类型,auto以便它始终与传递的函数的返回类型匹配。
贾斯汀时间-恢复莫妮卡

1
[从技术上讲,还有另一种方法,但是它使用模板魔术来做完全相同的事情,并且仍然需要使用记录功能来auto跟踪返回类型。]
贾斯汀·时报-恢复莫妮卡

7

它与功能的简单性无关(因为现在应该删除此问题的副本)。

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


3

考虑一个实际的生产环境:许多功能和单元测试都依赖于的返回类型foo()。现在,假设无论出于何种原因都需要更改返回类型。

如果返回类型auto无处不在,并且在获取返回值时foo()使用的调用者和相关函数auto,则需要进行的更改很小。如果不是这样,可能意味着数小时的工作非常繁琐且容易出错。

作为一个真实的例子,我被要求将模块的使用范围从原始指针改为智能指针。修复单元测试比实际代码要痛苦得多。

尽管还有其他方法可以处理,但使用auto返回类型似乎很合适。


1
在这些情况下,写会更好decltype(foo())吗?
Oren S

就我个人而言,我觉得typedefs和别名声明(即using类型声明)在这些情况下更好,特别是在类作用域内。
MAChitgarha

3

我想提供一个示例,其中返回类型自动是完美的:

假设您要为一个较长的后续函数调用创建一个简短的别名。使用auto时,您无需照顾原始的返回类型(以后可能会更改),用户可以单击原始函数来获取实际的返回类型:

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

PS:取决于这个问题。


2

对于场景3,我将函数签名的返回类型与要返回的局部变量一起使用。对于客户端程序员来说,函数返回将更加清楚。像这样:

方案3 为防止冗余:

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

是的,它没有auto关键字,但是其主体是相同的,以防止冗余并为无法访问源代码的程序员提供更轻松的时间。


2
IMO更好的解决方法是为打算用a表示的概念提供特定于域的名称vector<map<pair<int,double>,int>,然后使用该名称。
davidbak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.