如何避免 C++ 编译失败?

Blog
Author:
Dori ExtermanDori Exterman
Published On:
6月 13, 2021
Estimated reading time:
1 minutes

C++ “条条大路通罗马”

使用 C++ 几十年后,就慢慢掌握了 C++ 中“条条大路通罗马”的原理。任何事情都有多种解决方案,反之,造成编译失败的原因也是多种多样。本篇博客野心不小,因为我想试去厘清一些避免 C++ 编译错误的方法。但正本清源,要规避错误,首先要了解错误的源头。

愚蠢的做法:把失败简单归咎于编译器

的确,编译器有可能导致编译失败。不过,触发编译器的推手是你——程序员,做了一些不正确的操作。当程序员提供C++程序,编译器无法转换为机器代码时,编译失败。

说得更明白些?让我举一个编译失败的典型例子:

void ConvertStringToPasswordForm(char password[])

{

      while (*password != '\0') *password++ = '*';

}

这个函数的驱动程序如下

int main(int argc, char** argv)

{    

      char* password  = "MyTopSecretPasswordPublishedInABlog:-)";

      ConvertStringToPasswordForm(password);

      std::cout << "Password :: " << password << std::endl;

}

错误提醒信息说得很清楚:

error C2664: ‘void ConvertStringToPasswordForm(char [])’: cannot convert argument 1 from ‘const char *’ to ‘char []’

所以这是一个 const 问题,于是你想当然地把 driver 改成:

int main(int argc, char** argv)

{    

const char* password  = "MyTopSecretPasswordPublishedInABlog:-)";

      ConvertStringToPasswordForm(password);

      std::cout << "Password :: " << password << std::endl;

}

但编译器是无法变通的,编译失败的问题仍然没有解决。现在,cpp 编译错误消息为:

error C2664: ‘void ConvertStringToPasswordForm(char [])’: cannot convert argument 1 from ‘const char *’ to ‘char []’

可能有一瞬间,你想到将第一个函数的参数更改为 const char* 以修复编译失败,但最后还是放弃了。作为一名优秀 C++ 程序员,你最终决定关闭编译器:

int main(int argc, char** argv)

{    

const char* password  = "MyTopSecretPasswordPublishedInABlog:-)";

ConvertStringToPasswordForm(const_cast<char*>(password));

      std::cout << "Password :: " << password << std::endl;

}

程序进行编译了吗?当然,但编译结果,显然是失败!

那么,
解决这个 cpp 编译问题的正确方法是什么?正确方案如下:

int main(int argc, char** argv)

{    

      char password[]  = "MyTopSecurePasswordPublishedInABlog:-)";

      ConvertStringToPasswordForm(password);

      std::cout << "Password :: " << password << std::endl;

}

不过,搞定编译器还不够,我们需要理解程序运行的原理(可尝试这组程序:https://coliru.stacked-crooked.com/a/5e246877801d5263).

speed up c++

避免编译失败的具体操作

1.理解语言。在 C C++ 中,数组名会退化为指针,但并不总是像上面例子中呈现的那样。在任何语言中,避免编译失败的最重要方法就是充分理解该语言。

2.语法也在变化。让我用一个例子来解释:

int main(int argc, char *argv[])

{

    for (int i = 0; i < 10; ++i) { /*do something */ }

    int valueof_i = i;

    return 0;

}

我希望这能在旧版的 Dev-C++IDE 上运行,但事实并非如此。所以,我得到以下 C++ 编译错误:

compilation failure 1

该程序将是有效的,因为在之前,for 循环的索引变量作用域扩展到外部。因此,如果在新 ISO for 作用域导入之前,旧的 C++ 程序进行了迁移,那么 CPP 编译问题就会出现。在 Visual Studio 中,我们可以关闭一致性(不建议新代码使用):

compilation failure 2

  1. IDE 是友军。无论免费还是付费,大部分IDE 的功能都很出色。下面举一个例子,说明如何使用IDE帮助编译:

例如:

只要加上大括号, Visual Studio 2019 IDE 就会帮助完成编译。

{};

但,还记得你有多少次因为忘记分号而导致编译失败?不过,IDE 能自动修正,进而避免编译失败,这功能简直太友好!此外,优秀的 IDE 还有关键字突出显示、智能提示(IntelliSense)和上下文感应(context-sensitive)等功能。

  1. 保持好的工作习惯。假设你正在处理一个复杂的程序,使用自下而上或自上而下的方式编码,容易理解和编译的小函数能帮助你将工作简单化。在编写代码时,要随时保持程序编译干净、整洁。
  2. 格外注意模板化代码编写。模板元编程是图灵完备(Turing complete)的,因此编写元程序很有意思。但编译器编写者是否也希望模板错误是图灵完备的?不过,最新的编译器(支持 C++17 std 及更高版本)越来越能精确地定位错误。

例如,下列代码:

template<typename T>

class SimpleTemplateUse

{

private:

      const T& v;

public:

      SimpleTemplateUse(const T& v) :v(v) {}

};




template<typename T>

int f(T x)

{

      f(SimpleTemplateUse<T>(x));

}

在例证这个模板为f(0) 时,旧的 Dev-C++ IDE 显示出现以下错误:

compilation failure 3

同样的程序,VS 2019 给出了一些合理的解释:

fatal error C1202: recursive type or function dependency context too complex compilation failure error. Much better!

  1. 在尝试使用第三方库之前,请先了解其 API。这个建议不仅限于避免 C++ 编译错误,更是适用于所有支持第三方库编译的编程语言。如果不知道在何处使用 HINSTANCEHANDLE HMODULE,就不能使用 win32 API。所以,在深入研究复杂的库之前,请先详细了解各种信息。
  2. 如果你的程序打算跨平台运行,请确保平台相关代码在条件编译中得到很好的封装。我最近正在浏览 folly 的源代码,这是一个很完美的案例。跨平台部件封装良好:
#ifndef _WIN32

#define _GNU_SOURCE 1

#include <dlfcn.h>

#endif

以上代码可在 folly\ClockGettimeWrappers.cpp 中找到。因为它们支持 macOSLinux Windows,所以条件编译很重要。如果程序是跨平台的,最好不要使用任何不同的编译器厂商提供的 C++ 专有扩展。

8.遵循良好的C++编码标准。除了C++的创建者,“C++ 内部是一个更安全的语言环境,但大家都挣扎着要出来”。当你使用像 CPPCoreGuidelines,或是谷歌编码风格等好的编码标准时,可以自动将限制在更小、更安全的语言中,从而减少不必要的编译错误。

  1. 今天的警告就是明天错误。所以,重视警告,这不仅仅是让软件编译能够经得起未来的重重考验,而且作为一种良好的工作习惯。在开发过程中,我们启用所有警告并认真地修复,这些话都已经说腻了。
  2. 使用容器技术(如 Docker)修复依赖关系。Docker 的一个重要用例是从基础映像开始复制构建环境。当然,编写一个好的 Dockerfile 需要专业知识,不过这些都是一劳永逸的工作。
  3. 使用 CMake 或任何此类构建生成器来自动化构建(阅读有关 CMake 生成器的更多内容)。IDE 有利于软件开发,但在不需要人工干预的情况下实现构建的自动化是很重要的。具体请看下一条。
  4. 设置签入/合并/滚动生成策略。确保任何分支合并都与自动生成同步,如果出现错误,该自动生成将导致合并失败,避免渗入分支的错误不会到处蔓延。

我提醒自己,我写的是一篇博客,而不是书。文章的篇幅已经够长,因此我需要简单归纳一下。

总结

下图很好地总结了文中提到避免编译错误的方法:

compilation failure - summary