在 C + + 中使用 auto 声明变量有什么缺点吗?

似乎 auto是 C + + 11中添加的一个相当重要的特性,它似乎遵循了许多较新的语言。与 Python 这样的语言一样,我没有看到任何显式的变量声明(我不确定是否可以使用 Python 标准)。

使用 auto声明变量而不是显式声明它们有什么缺点吗?

16836 次浏览

它使您的代码读起来有点困难或乏味。 想象一下这样的事情:

auto output = doSomethingWithData(variables);

现在,为了找出输出的类型,您必须跟踪 doSomethingWithData函数的签名。

关键字 auto只是简单地从返回值推断类型。

# Python
a
a = 10       # OK
a = "10"     # OK
a = ClassA() # OK


// C++
auto a;      // Unable to deduce variable a
auto a = 10; // OK
a = "10";    // Value of const char* can't be assigned to int
a = ClassA{} // Value of ClassA can't be assigned to int
a = 10.0;    // OK, implicit casting warning

由于 auto是在编译过程中推导出来的,因此在运行时不会有任何缺点。

问题是关于 auto的缺点,所以这个答案强调了其中的一些。使用编程语言特性的缺点(在这种情况下,与语言关键字相关联的工具)并不意味着特性是不可接受的,也不意味着应该完全避免使用特性。这意味着优势与劣势并存,因此决定使用 auto类型演绎优于替代方案时必须考虑工程折衷。

当使用得当时,auto也有几个优点-这不是问题的主题。这些缺点源于容易被滥用,以及代码以意想不到的方式行为的可能性增加。

主要的缺点是,通过使用 auto,您不一定知道正在创建的对象的类型。还有一些情况下,程序员可能期望编译器推断出一种类型,但编译器坚定地推断出另一种类型。

给出一个声明

auto result = CallSomeFunction(x,y,z);

你不一定知道 result是什么类型。可能是 int。可能是个指针。可能是别的原因。所有这些都支持不同的操作。您还可以通过一个小小的更改来戏剧性地改变代码,如

auto result = CallSomeFunction(a,y,z);

因为,取决于 CallSomeFunction()存在什么样的重载,结果的类型可能完全不同——因此后续代码的行为可能完全不同于预期。您可能会在以后的代码中突然触发错误消息(例如,随后尝试取消对 int的引用,尝试更改现在是 const的内容)。更危险的变化是您的变更通过编译器,但是后续的代码行为在不同的和未知的-可能是错误的-方式。例如(正如 sashoalm 在注释中指出的那样) ,如果一个变量的推导类型将一个整数类型更改为一个浮点类型,随后的代码会意外地受到精度损失的影响。

因此,如果没有某些变量类型的外显知识,就很难严格证明代码是按预期工作的。这意味着在高关键性(例如安全关键或任务关键)领域需要更多的努力来证明“适合用途”的说法。

另一个更常见的缺点是,程序员容易使用 auto作为强制编译代码的钝器,而不是考虑代码在做什么,并努力使之正确。

从原则上讲,这并不是 auto的缺点,但从实际操作的角度来看,这似乎对某些人来说是个问题。基本上,有些人要么: a)把 auto当作类型的救世主,在使用它时关闭他们的大脑,或者 b)忘记了 auto总是推导出值类型。这导致人们做这样的事情:

auto x = my_obj.method_that_returns_reference();

哎呀,我们刚刚深度复制了一些对象。这通常是一个错误或性能失败。然后,你也可以转向另一个方向:

const auto& stuff = *func_that_returns_unique_ptr();

现在你得到了一个悬而未决的推荐信。这些问题根本不是由 auto引起的,所以我不认为它们是反对 auto的合理论据。但似乎 auto确实使这些问题更为常见(根据我的个人经验) ,原因我在开头列出。

我认为给定的时间,人们会调整,并理解分工: auto推断的基本类型,但你仍然要考虑参考和常量。但这需要一点时间。

其他的回答是提到缺点,比如“你不知道变量的类型”我认为这很大程度上与代码中的马虎变数命名原则有关。如果接口的名称很清楚,那么就不需要知道 关心的确切类型。当然,auto result = callSomeFunction(a, b);没有告诉你太多。但是 auto valid = isValid(xmlFile, schema);告诉你的信息足以让你使用 valid而无需关心它的确切类型是什么。毕竟,只有 if (callSomeFunction(a, b)),你也不会知道类型。其他子表达式临时对象也是如此。所以我不认为这是 auto真正的缺点。

我想说它的主要缺点是,有时候,返回类型的确切值是您希望使用的 没有。实际上,有时实际的返回类型与“逻辑”返回类型在实现/优化细节上不同。表达式模板就是一个很好的例子。假设我们有这个:

SomeType operator* (const Matrix &lhs, const Vector &rhs);

逻辑上,我们希望 SomeTypeVector,并且我们肯定希望在我们的代码中这样对待它。但是,出于优化的目的,我们使用的代数库可能实现了表达式模板,实际的返回类型是:

MultExpression<Matrix, Vector> operator* (const Matrix &lhs, const Vector &rhs);

现在的问题是,MultExpression<Matrix, Vector>很可能在内部存储 const Matrix&const Vector&; 它期望在完整表达式结束之前将其转换为 Vector。如果我们有这个代码,一切都很好:

extern Matrix a, b, c;
extern Vector v;


void compute()
{
Vector res = a * (b * (c * v));
// do something with res
}

然而,如果我们在这里使用 auto,我们可能会遇到麻烦:

void compute()
{
auto res = a * (b * (c * v));
// Oops! Now `res` is referring to temporaries (such as (c * v)) which no longer exist
}

正如我在 这个答案 auto中所描述的,有时候可能会导致你意想不到的奇怪情况。 您必须显式地说 auto&有一个引用类型,而只做 auto可以创建一个指针类型。这可能会导致混淆,因为省略了所有的说明符,从而导致引用的副本而不是实际的引用。

到目前为止这里没有人提到,但是如果你问我的话,这个问题值得回答。

因为(即使每个人都应该意识到 C != C++)用 C 编写的代码可以很容易地被设计成为 C + + 代码的基础,因此不需要花费太多精力就可以与 C + + 兼容,这可能是设计的一个要求。

我知道一些规则,其中来自 C的一些定义良好的构造对于 C++是无效的,反之亦然。但是这只会导致可执行文件被破坏,并且已知的 UB 子句会被应用,这种情况大多数时候会被奇怪的循环所注意到,从而导致崩溃或者其他什么(或者甚至可能保持不被发现,但是这并不重要)。

但是 auto是第一次 1这个变化!

假设您在传输代码之前使用 auto作为存储类说明符。它甚至不一定(取决于它的使用方式)“中断”; 它实际上可以悄悄地改变程序的行为。

我们应该记住这一点。


至少在我第一次意识到的时候。

缺点之一是有时无法用 auto声明 const_iterator。在这个取自 这个问题的代码示例中,您将获得普通(非 const)迭代器:

map<string,int> usa;
//...init usa
auto city_it = usa.find("New York");

我能想到的一个原因是,您失去了强制返回的类的机会。如果您的函数或方法返回一个长64位,并且您只需要一个32无符号整型,那么您就失去了控制它的机会。

auto没有缺点 本质上,我主张在新代码的任何地方都使用它。它允许您的代码始终如一地进行类型检查,并始终如一地避免无声切片。(如果 BA派生,返回 A的函数突然返回 B,那么 auto的行为就像预期的那样存储它的返回值)

尽管如此,前 C + + 11遗留代码可能依赖于使用显式类型变量引起的隐式转换。将显式类型的变量更改为 auto可能会改变代码行为,所以你最好小心点。

这个开发者一样,我讨厌 auto,或者更确切地说,我讨厌人们滥用 auto

我的(强烈)意见,auto是为了帮助您编写泛型代码,不是为了减少打字
C + + 是一种语言,其目标是让您编写健壮的代码,没有以最小化开发时间。
从 C + + 的许多特性中可以很明显地看出这一点,但不幸的是,一些较新的特性(如 auto)减少了输入,从而误导人们认为他们应该开始懒于输入。

auto之前的日子里,人们使用 typedef,这非常好,因为 typedef允许库的设计者帮助你找出返回类型应该是什么,这样他们的库就可以像预期的那样工作。当您使用 auto时,您可以从类的设计器获得 夺走控制权,然后请求 编译器确定类型应该是什么,这样就从工具箱中移除了最强大的 C + + 工具之一,从而使 崩溃了的代码面临风险。

一般来说,如果使用 auto,应该是因为 您的代码适用于 < strong > any  合理类型不是,因为您太懒了,不愿意写下它应该使用的类型。 如果使用 auto作为帮助懒惰的工具,那么最终会在程序中引入 细微的虫子,这通常是由于使用了 auto而没有发生的隐式转换引起的。

不幸的是,这里的一个简短示例中的 这些错误很难说明,因为它们的简洁性使它们不如用户项目中出现的实际示例那么令人信服——然而,它们很容易出现在预期发生某些 隐式转换的模板繁多的代码中。

如果你想要一个例子,有一个 这里。但是,需要注意的是: 在试图跳过和批评代码之前: 请记住,许多著名的和成熟的库都是围绕这种隐式转换开发的,而 他们之所以存在,是因为他们能够解决即使不是不可能也是困难的问题则是为了解决其他问题而开发的。在批评他们之前,试着弄清楚一个 更好的解决办法

另一个恼人的例子是:

for (auto i = 0; i < s.size(); ++i)

生成一个警告(comparison between signed and unsigned integer expressions [-Wsign-compare]) ,因为 i是一个带符号的 int。

for (auto i = 0U; i < s.size(); ++i)

或者更好:

for (auto i = 0ULL; i < s.size(); ++i)

我认为 auto在本地化上下文中使用是很好的,读者可以很容易和明显地推断出它的类型,或者通过它的类型的注释或者推断出实际类型的名称来很好地记录。那些不理解它如何工作的人可能会以错误的方式使用它,比如用它来代替 template或类似的。在我看来,这里有一些好的和坏的用例。

void test (const int & a)
{
// b is not const
// b is not a reference


auto b = a;


// b type is decided by the compiler based on value of a
// a is int
}

好的用途

迭代器

std::vector<boost::tuple<ClassWithLongName1,std::vector<ClassWithLongName2>,int> v();


..


std::vector<boost::tuple<ClassWithLongName1,std::vector<ClassWithLongName2>,int>::iterator it = v.begin();


// VS


auto vi = v.begin();

函数指针

int test (ClassWithLongName1 a, ClassWithLongName2 b, int c)
{
..
}


..


int (*fp)(ClassWithLongName1, ClassWithLongName2, int) = test;


// VS


auto *f = test;

坏用途

数据流

auto input = "";


..


auto output = test(input);

函数签名

auto test (auto a, auto b, auto c)
{
..
}

小案件

for(auto i = 0; i < 100; i++)
{
..
}

我很惊讶没有人提到这一点,但假设你正在计算某事的阶乘:

#include <iostream>
using namespace std;


int main() {
auto n = 40;
auto factorial = 1;


for(int i = 1; i <=n; ++i)
{
factorial *= i;
}


cout << "Factorial of " << n << " = " << factorial <<endl;
cout << "Size of factorial: " << sizeof(factorial) << endl;
return 0;
}

这段代码将输出以下内容:

Factorial of 40 = 0
Size of factorial: 4

这绝对不是预期的结果。之所以会发生这种情况,是因为 auto推断出可变阶乘的类型为 int,因为它被分配给了 1