【问题标题】:Omit return type in C++11在 C++11 中省略返回类型
【发布时间】:2010-12-24 01:05:45
【问题描述】:

我最近发现自己在 C++11 模式下在 gcc 4.5 中使用了以下宏:

#define RETURN(x) -> decltype(x) { return x; }

然后编写这样的函数:

template <class T>
auto f(T&& x) RETURN (( g(h(std::forward<T>(x))) ))

我一直这样做是为了避免不得不有效地编写两次函数体,并使函数体中的更改和返回类型保持同步(在我看来这是一场等待发生的灾难)。

问题是这种技术只适用于单行函数。所以当我有这样的事情时(复杂的例子):

template <class T>
auto f(T&& x) -> ...
{
   auto y1 = f(x);
   auto y2 = h(y1, g1(x));
   auto y3 = h(y1, g2(x));
   if (y1) { ++y3; }
   return h2(y2, y3);
}

然后我必须在返回类型中添加一些可怕的东西。

此外,每当我更新函数时,我都需要更改返回类型,如果我没有正确更改它,如果幸运的话,我会得到一个编译错误,或者在更糟糕的情况下。我觉得必须将更改复制并粘贴到两个位置并保持同步并不是一个好习惯。

而且我想不出我想要在返回时进行隐式转换而不是显式转换的情况。

当然有一种方法可以让编译器推断出这些信息。编译器保守秘密的意义何在?我认为 C++11 的设计是为了不需要这样的重复。

【问题讨论】:

  • 如果你有这么多使用 decltype 的函数,也许你需要重新考虑你正在编写的代码。我们在没有 decltype 的情况下存活了很长时间......
  • (你听起来好像每隔一小时就更改一次函数。)是的,如果编译器可以自动推断返回类型会很好,但它根本不存在,并且有一点你必须停止尝试成为编译器,而只是编写它接受的代码。 (是的,C++0x 减轻了很多事情,但不幸的是,它不能全力以赴。)所以只需指定返回类型。
  • 我不认为所有的负面 cmets 都是合理的。 Boost 库中充满了诸如 ::return_type 之类的函数对象来解决这些问题。你真的认为必须定义一个新类来定义函数的返回类型是“可读的”吗?!我看不出不必以三种不同的方式复制相同的信息有什么难以理解的。
  • @ybungalobill:你能简单地推断出 boost 融合向量的返回类型吗?如果不是,你认为他们的设计有问题吗?如果是这样,你会怎么做?
  • 引入 decltype 和惰性返回类型语法是有原因的,这是因为预先确定许多涉及转发的函数的类型是不可能或难以置信的。 @SoapBox:我以尊重的方式完全不同意。

标签: c++ c++11 return-value


【解决方案1】:

看来g++ 4.8 正在实现自动返回类型推导。 该补丁是由 Jason Merrill 提供的,他还为该功能发送了 C++-1Y 的论文。该功能适用​​于 -std=c++1y。

还在玩。

【讨论】:

  • 返回类型推导已投进C++14。 latest paper 有详细信息。
【解决方案2】:

草案 8.3.5p12 中给出了这种行为的基本原理:

尾随返回类型最有用 对于一个会更多的类型 复杂的指定之前 声明符ID:

template <class T, class U> auto add(T t, U u) -> decltype(t + u);

而不是

template <class T, class U> decltype((*(T*)0) + (*(U*)0)) add(T t, U u);

所以这实际上只是为了简化引用参数名称的情况。

如果您假设 C++ 可以总是从函数体中推断出函数的返回类型:这是行不通的。 C++(和C)的目标是通过将声明与实现分开来实现模块化,因此在调用时,您可能没有可用的函数体。但是,每个调用者都需要知道被调用的每个函数/方法的参数类型和返回类型

【讨论】:

    【解决方案3】:

    如果您只是尝试设置返回类型,请将其设为模板参数。这样,您可以更改与返回类型相关的所有内容,而无需实际更改函数。如果您愿意,可以在此示例中设置默认返回类型。

    template <class R = int, class T>
    R f(T&& x)
    {
       ...
       return h2(y2, y3);
    }
    

    下面的代码证明了它的有效性。

    演示代码:

    #include <iostream>
    #include <iomanip>
    
    template <class T, class S>
    T h2(T& x, S& y)
    {
      return x + y;
    }
    
    template <class R = int, class T>
    R f(T& x)
    {
      auto y2 = x;
      auto y3 = x;
      return h2(y2, y3);
    }
    
    int main(int argc, char** argv)
    {
      int x = 7;
      std::string str = "test! ";
    
      auto d = f<double>(x);
      auto i = f(x); // use default type (int)
      auto s = f<std::string>(str);
    
      std::cout << std::fixed << std::setprecision(4);
      std::cout << "double: " << d << std::endl;
      std::cout << "int: " << i << std::endl;
      std::cout << "string: " << s << std::endl;
    
      return 0;
    }
    

    输出:

    double: 14.0000
    int: 14
    string: test! test!
    

    不幸的是,您正在寻找的确切功能不存在(目前)并且不属于 C++0x 规范的一部分。然而,在起草时,这可能是 C++1x 规范的一部分。在那之前,坚持使用模板。

    【讨论】:

      【解决方案4】:

      编辑:哎呀,我刚刚意识到 trailing-return-type 说明符和 return 语句之间存在范围差异。具体来说:

      auto f(int a)
      {
          char r[sizeof(f(a))+1];
          return r;
      }
      

      咔嚓!


      上一个答案:

      不幸的是,在这种情况下,该语言没有提供让编译器推断返回类型的语法,因为证明推断是可能的很简单。

      具体来说,我们讨论的是函数内部只有一个 return 语句的情况。

      不管 return 语句在函数中的哪个位置,或者前面的代码有多复杂,都应该清楚以下转换是可能的:

      return (ugly expression);
      

      进入

      auto return_value = (ugly expression);
      return return_value;
      

      如果编译器可以推断出return_value的类型(并且根据C++0x规则是可以的),那么可以选择return_value的推断类型作为函数的返回类型。

      因此,在我看来,对 C++0x 进行修改,其中仅当返回语句的多重性不完全是一个时才需要尾随返回类型说明符,这将是可行的并且可以解决问题。

      【讨论】:

      • 但是你可以写auto f = [&amp;](int a) { char r[sizeof(f(a))+1]; return r; };,如果你这样做编译器只会给你一个错误,例如,“错误:用'auto'类型声明的变量'f'不能出现在它自己的初始化程序中”
      【解决方案5】:

      我同意 Yttrill。返回类型推导在 Haskell 等语言中已经被证明是一种可行的做法,并且由于 C++ 已经实现了“自动”,因此实现返回类型推导只是进一步的一步。这种推论应该发生在特化时,而不是模板定义时,因为需要提供给模板的真实类型的信息。在泛型 C++ 中,声明和定义的分离不再是常见的做法,因为模板主体必须写在头文件中,因此模板主体几乎总是与模板声明一起使用。在有多个返回语句并且它们的类型不匹配的情况下,编译器可以愉快地报告错误。总之,如果委员会愿意,C++ 中的返回类型推导是完全可能的。而且这非常重要,因为重复手动编写返回类型会阻碍小型泛型辅助函数的普遍使用,而这在函数式和泛型编程中非常普遍。

      【讨论】:

      • 模板主体并不一定要写在头文件中:您可以像往常一样在 .cpp .cxx 中编写主体,并为您可以使用的实例显式实例化它。这并不适用于所有用例,但通常确实有助于减少编译时间并提高可读性。
      【解决方案6】:

      包括 Ocaml 和 Felix 在内的许多编程语言都可以推断出函数的返回类型,并且不需要指定它。在 Ocaml 中,您可以而且必须在界面中指定它。在 Felix 中,我发现对于某些函数,明智的做法是在库代码中指定它以使其更易于使用。

      我很惊讶“自动”不适用于返回类型,它肯定在盘子上。这是否被认为太难以实施? [考虑到函数可以递归,这不是微不足道的]。

      哦……现在我明白了。问题是愚蠢的模板设计功能“依赖名称”:

      template<class T> auto f(T a) { return a.f(); }
      

      因此,虽然由于重载而计算普通函数的返回类型有点棘手,但由于依赖名称查找,模板是不可能的:它只能在实例化后完成。但是超载必须在此之前发生。所以自动返回类型不能在语言中使用,因为它不能泛化到模板。

      【讨论】:

      • Yttrill:你能详细说明一下吗? "template class decltype(a.f()) f(T a) { return a.f(); } 会失败吗?(这是我的宏所做的)
      • 您缺少返回类型的“自动”意味着您需要使用尾随返回类型声明 (-> T)。
      • 如果你有多个不同类型的return语句,你需要找到所有指定类型的超集。我认为这个特性会变得不必要地复杂,并导致很多神秘的错误和极端情况:(例如,你将指针 A*B* 返回到前向声明的类,如果只有前向声明,它们实际上是从另一个类派生的可用,扣费失败)。
      • 在 C++14 中,这是可能的。
      【解决方案7】:

      你的返回类型真的经常改变吗?为什么不能明确指定它? 示例:

      template<class T>
      int f(T&& x)
      {
      ...
      }
      

      人们这样做了二十多年......

      【讨论】:

      • 返回类型类似于 decltype(h2(h(f(x),g1(x),h(f(x),g2(x)))。我可以明确地写出来,但这是混乱的重复。
      • 使用右值引用,这通常是不可能的。设计这种语法的全部原因是因为不可能提前知道返回类型。
      • 但是...为什么每次编辑函数时都更改返回类型? “此外,每当我更新函数时,我都需要更改返回类型”
      • 不,您无法提前确定类型。这取决于T。请记住,g1()g2()h()h2() 都可以是模板函数并且可以被特化。为任何其他函数添加新的特化可能会更改 f() 的返回类型,并且不会对 f() 本身进行任何修改
      • 多年来人们甚至拥有 C++!为什么我们需要C++,你总是可以使用C,甚至汇编。代码重复被称为有史以来最好的编程实践!
      猜你喜欢
      • 2015-05-11
      • 1970-01-01
      • 2021-05-25
      • 2018-10-19
      • 1970-01-01
      • 1970-01-01
      • 2012-12-05
      • 1970-01-01
      • 2021-02-05
      相关资源
      最近更新 更多