漫谈C++——谈谈在函数中使用auto

C++学习笔记

Posted by Felix Zhang on October 30, 2020

谈谈在函数中使用auto

前边我写了一篇文章关于如何在变量中使用auto,今天我们来看看在函数中使用auto时的场景,以及需要注意的细节。

基本上,在函数中使用auto的情形大致可分为两类,在C++11中,auto被引入放到函数声明中返回类型的位置,用间接的方式来定义函数的返回类型,如下:

//等价于 std::string someFunc(int i, double j);
auto someFunc(int i, double j) -> std::string;

在C++14中,编译器可以直接推断出函数的返回类型了,所以可以写成下边的样子:

auto someFunc(int i, double j) {
  //自动推断返回类型为std::string
  return std::to_string(i + j);
}

尾置返回类型

上边C++11的例子并没有给我们带来很多直观的感受——auto在其中到底有什么用?既然我们还是要显示声明函数的返回类型,而且和更传统的返回类型表述相比,我们为了实现尾置还要再多写一个auto->,更重要的是,这种声明看起来真的太丑了,我们为什么要用这种表述方式呢?

在很多返回类型要取决于参数类型的时候,比如在函数模板中,上边这种写法就会有很大作用,因为你很有可能并不知道进行某种操作后自己会得到什么类型,请看下例:

template<typename T, typename V>
auto addWithTwoTypes(T t, V v) -> decltype(t + v) {
  return t + v;
}

上边这个函数会返回T类型变量和V类型变量的和,如果TV分别是shortint,那么返回类型就会自动推断为int,但是如果一个是double一个是int,那么返回类型就会是double。因此,返回值类型和两个模板类型都相关。

如果将上述例子写成如下形式,不使用auto,可不可行呢?

template<typename T, typename V>
decltype(t + v) addWithTwoTypes(T t, V v) {
  return t + v;
}

答案是否定的,因为在推导decltype(t + v)tv还没定义,因此会有类似“模板未实例化”的报错。

再举一个例子,如果有如下定义:

class JackRoseCreator {
public:
	Jack giveMeJack();
  Rose giveMeRose();
};

Baby operator+(Jack const& jack, Rose const& rose);

template<typename T>
auto giveMeSomething(T const& t) -> decytype(t.giveMeJack() + t.giveMeRose()) {
  return t.giveMeJack() + t.giveMeRose();
}

上边这个例子就是第一个例子的复杂版,在我们的auto写法中,最后auto会被编译器推导为一个Baby类型,但是其他写法可能就没这么简单了。

另外一个不常用的地方在于,使用尾置返回类型也可简化一些函数的写法,比如:

int (*generatorArr(int i))[10086];	//返回一个指向大小为10086的int类型的数组的指针

上边函数可以用auto来简化成:

auto generatorArr(int i) -> int (*)[10086];

是不是看起来更方便一些?

返回类型推导

在C++14中,编译器终于可以自己推导任何函数的返回类型了,无论多复杂的都可以。唯一的条件就是,在单一的返回语句中,返回的类型必须在编译期时确定的,其他的规则就和在变量中使用auto一模一样了。

因为在推导类型时,编译器必须需要知道函数的定义,也就是说,这种用法被限制在了内联函数、函数模板以及lambda表达式中。对于一个在头文件中声明、在其他文件中实现的函数来说,auto这样的用法是不可行的。然而,内联函数、函数模板以及lambda表达式这三种情况,也足够应付你需要以及应该使用auto的地方了。

我说了“应该”,因为就像变量的自动推导一样,函数返回类型的自动推导可以避免不必要的转换,以及事后修改变量类型时对代码所做的必要的改动。请看下例:

class Container {
  typedef std::vector<int> Container_t;
  Container_t vals;
  
public:
  auto begin() const {
    return std::begin(vals);
  }
  
  auto at(Containter_t :: size_type id) const {
    return vals[id];
  }
  //...
};

事后变量类型的更改指的是,也许你觉得vector不是最好的选择呢?或许你觉得int不够用了呢?没关系,只需要把int改为long long就好了,剩下的成员函数都可以原封不动地留在那里。

有了返回类型的推导,大多数的尾置用法都可以被替换了,比如上边的例子就可以改写为:

template<typename T>
auto giveMeSomething(T const& t) {
  return t.giveMeJack() + t.giveMeRose();
}

简洁明了。

作为代码的阅读者来说,他们也希望像编译器一样一眼就看到有返回类型推导的函数的return语句,这也就是说你的函数应该尽可能的短——当然,函数短小精悍是一个普遍的要求,只是有返回类型推导的函数更应该注意便是了。

结论

就以上两种用法作总结,如果条件允许,尽可能的去使用返回类型推导吧,这样做会让你的变量类型上下一致性更高。但是至于尾置返回类型,还是要尽可能的避免,因为他们的语法实在是太不美观,同样让人读起来也很困难。