Skip to content

Latest commit

 

History

History
706 lines (463 loc) · 23.9 KB

File metadata and controls

706 lines (463 loc) · 23.9 KB

十、收集平台和编译器信息

在本章中,我们将介绍:

  • 检测操作系统和编译器
  • 检测 int128 支持
  • 检测并旁路禁用的 RTTI
  • 使用更简单的方法编写元函数
  • 在 C++ 11 中减少代码大小并提高用户定义类型的性能
  • 导出和导入函数和类的可移植方式
  • 检测 Boost 版本并获取最新功能

介绍

不同的项目和公司有不同的编码要求。其中一些禁止异常或 RTTI,而一些禁止 C++ 11。如果你愿意写可移植的代码,可以被广泛的项目使用,这一章是为你准备的。

想让你的代码尽可能快,使用最新的 C++ 特性吗?您肯定需要一个工具来检测编译器特性。

有些编译器有独特的功能,可能会大大简化你的生活。如果您的目标是单一编译器,您可以节省很多时间并使用这些功能。不需要从头实现它们的类似物!

本章专门介绍用于检测编译器、平台和 Boost 特性的不同助手宏。这些宏在 Boost 库中被广泛使用,对于编写能够处理任何编译器标志可移植代码是必不可少的。

检测操作系统和编译器

我猜你已经看到了一堆丑陋的宏来检测编译代码的编译器。类似这样的事情是 C 语言中的典型做法:

#include <something_that_defines_macros>
#if !defined(__clang__) \
    && !defined(__ICC) \
    && !defined(__INTEL_COMPILER) \
    && (defined(__GNUC__) || defined(__GNUG__))

// GCC specific

#endif

现在,试着想出一个好的宏来检测 GCC 编译器。尽量缩短宏的使用时间。

看看下面的食谱来验证你的猜测。

准备好

只需要 C++ 的基础知识。

怎么做...

食谱很简单,由一个标题和一个宏组成。

  1. 标题:
#include <boost/predef/compiler.h>
  1. 宏:
#if BOOST_COMP_GNUC

// GCC specific

#endif

它是如何工作的...

标题<boost/predef/compiler.h>知道所有可能的编译器,并且为每个编译器都有一个宏。所以如果当前编译器是 GCC,那么宏BOOST_COMP_GNUC定义为1,其他编译器的所有其他宏定义为0。如果我们不在 GCC 编译器上,那么BOOST_COMP_GNUC宏被定义为0

由于这种方法,您不需要检查正在定义的宏本身:

#if defined(BOOST_COMP_GNUC) // Wrong!

// GCC specific

#endif

Boost.Predef库的宏总是被定义的,这样你就不用在#ifdef中输入defined()def

还有更多...

Boost.Predef库还有检测 os、架构、标准库实现的宏,以及一些硬件能力。使用始终定义的宏;这允许您编写更短的复杂表达式:

#include <boost/predef/os.h>
#include <boost/predef/compiler.h>

#if BOOST_COMP_GNUC && BOOST_OS_LINUX && !BOOST_OS_ANDROID

// Do something for non Android Linux.

#endif

现在,最精彩的部分。Boost.Predef库可在 C、C++ 和 Objective-C 编译器上使用。如果你喜欢,在你的非 C++ 项目中使用它。

C++ 17 没有Boost.Predef库功能。

请参见

  • 阅读Boost.Predef的官方文档,了解更多关于其在http://boost.org/libs/predef的能力的信息
  • 下一个食谱将向你介绍Boost.Config库,那是很有秩序的,稍微不那么漂亮,但是功能要多得多

检测 int128 支持

一些编译器支持扩展算术类型,如 128 位浮点或整数。让我们快速浏览一下如何使用 Boost 来使用它们。

我们将创建一个接受三个参数并返回这些方法的乘积值的方法。如果编译器支持 128 位整数,那么我们就使用它们。如果编译器支持long long,那么我们就用它;否则,我们需要发出编译时错误。

准备好

只需要 C++ 的基础知识。

怎么做...

我们需要什么来处理 128 位整数?显示它们可用的宏和一些跨平台具有可移植类型名称的typedefs

  1. 包括标题:
#include <boost/config.hpp>
  1. 现在,我们需要检测 int128 支持:
#ifdef BOOST_HAS_INT128
  1. 增加一些typedefs并按如下方法执行:
typedef boost::int128_type int_t;
typedef boost::uint128_type uint_t;

inline int_t mul(int_t v1, int_t v2, int_t v3) {
    return v1 * v2 * v3;
}
  1. 对于不支持 int128 类型且没有long long的编译器,我们可能会产生编译时错误:
#else // #ifdef BOOST_HAS_INT128

#ifdef BOOST_NO_LONG_LONG
#error "This code requires at least int64_t support"
#endif
  1. 现在,我们需要使用int64为不支持 int128 的编译器提供一些实现:
struct int_t { boost::long_long_type hi, lo; };
struct uint_t { boost::ulong_long_type hi, lo; };

inline int_t mul(int_t v1, int_t v2, int_t v3) {
    // Some hand written math.
    // ...
}

#endif // #ifdef BOOST_HAS_INT128

它是如何工作的...

标题<boost/config.hpp>包含大量描述编译器和平台特性的宏。在本例中,我们使用BOOST_HAS_INT128检测对 128 位整数的支持,使用BOOST_NO_LONG_LONG检测对 64 位整数的支持。

从例子中我们可以看到,Boost 对于 64 位有符号和无符号整数有typedefs:

boost::long_long_type  
boost::ulong_long_type  

128 位有符号和无符号整数也有typedefs:

boost::int128_type 
boost::uint128_type 

还有更多...

C++ 11 通过long long intunsigned long long int内置类型支持 64 位类型。不幸的是,并不是所有的编译器都支持 C++ 11,所以BOOST_NO_LONG_LONG可能对你有用。

128 位整数不是 C++ 17 的一部分,所以typedefs和宏来自 Boost 是编写可移植代码的方式之一。

C++ 标准化委员会正在进行一项关于添加编译时指定宽度的整数的工作。当这项工作完成时,您将能够创建 128 位、512 位甚至 8388608 位(1 MB 大)整数。

请参见

检测并旁路禁用的 RTTI

一些公司和库对他们的 C++ 代码有特定的要求,比如没有 RTTI 的成功编译。

在这个小食谱中,我们不仅要检测禁用的 RTTI,还要从头开始编写一个类似 Boost 的库,存储关于类型的信息,并在运行时比较类型,即使没有typeid

准备好

这个食谱需要 C++ RTTI 用法的基本知识。

怎么做...

检测禁用的 RTTI、存储关于类型的信息以及在运行时比较类型是在 Boost 库中广泛使用的技巧。

  1. 为此,我们首先需要包含以下标题:
#include <boost/config.hpp> 
  1. 让我们首先看看启用了 RTTI 并且 C++ 11 std::type_index类可用的情况:
#if !defined(BOOST_NO_RTTI) \
    && !defined(BOOST_NO_CXX11_HDR_TYPEINDEX)

#include <typeindex>
using std::type_index;

template <class T>
type_index type_id() {
    return typeid(T);
}
  1. 否则,我们需要构建自己的type_index类:
#else

#include <cstring>
#include <iosfwd> // std::basic_ostream
#include <boost/current_function.hpp>

struct type_index {
    const char * name_;

    explicit type_index(const char* name)
        : name_(name)
    {}

    const char* name() const { return name_; }
};

inline bool operator == (type_index v1, type_index v2) {
    return !std::strcmp(v1.name_, v2.name_);
}

inline bool operator != (type_index v1, type_index v2) {
    return !(v1 == v2);
}
  1. 最后一步是定义type_id功能:
template <class T>
inline type_index type_id() {
    return type_index(BOOST_CURRENT_FUNCTION);
}

#endif
  1. 现在,我们可以比较类型:
#include <cassert>

int main() {
    assert(type_id<unsigned int>() == type_id<unsigned>());
    assert(type_id<double>() != type_id<long double>());
}

它是如何工作的...

如果禁用了 RTTI,则定义宏BOOST_NO_RTTI,如果编译器没有<typeindex>头,也没有std::type_index类,则定义宏BOOST_NO_CXX11_HDR_TYPEINDEX

上一节第三步手写的type_index结构只保存了某个字符串的指针;这里没什么有趣的。

看一下BOOST_CURRENT_FUNCTION宏。它返回当前函数的全名,包括模板参数、参数和返回类型。 例如,type_id<double>()表示如下:

 type_index type_id() [with T = double]

因此,对于任何其他类型,BOOST_CURRENT_FUNCTION返回不同的字符串,这就是为什么示例中的type_index变量与它不相等。

恭喜你!我们刚刚重新发明了大部分Boost.TypeIndex库功能。删除步骤 1 至 4 中的所有代码,并稍微更改步骤 5 中的代码以使用Boost.TypeIndex库:

#include <boost/type_index.hpp>

void test() {
    using boost::typeindex::type_id;

    assert(type_id<unsigned int>() == type_id<unsigned>());
    assert(type_id<double>() != type_id<long double>());
}

还有更多...

当然Boost.TypeIndex略多于此;它允许你以一种独立于平台的方式获得人类可读的类型名,解决与平台相关的问题,允许你发明自己的 RTTI 实现,拥有一个 constexpr RTTI,以及其他东西。

不同的编译器有不同的宏来获取完整的函数名。使用 Boost 中的宏是最便携的解决方案。BOOST_CURRENT_FUNCTION宏在编译时返回名称,因此它意味着最小的运行时损失。

C++ 11 有一个__func__魔法标识符,它被评估为当前函数的名称。但是__func__的结果只是函数名,而BOOST_CURRENT_FUNCTION也尽量显示函数参数,包括模板参数。

请参见

使用更简单的方法编写元函数

第 4 章编译时技巧第 8 章元编程都致力于元编程。如果你试图使用那些章节中的技巧,你可能已经注意到写一个元功能会花费很多时间。因此,在编写一个可移植的实现之前,使用更用户友好的方法来尝试元功能可能是一个好主意,比如 C++ 11 constexpr

在这个食谱中,我们来看看如何检测constexpr支持。

准备好

constexpr函数是可以在编译时计算的函数。对于这个食谱,我们只需要知道这些。

怎么做...

让我们看看如何检测编译器对constexpr特性的支持:

  1. 就像本章的其他食谱一样,我们从以下标题开始:
#include <boost/config.hpp> 
  1. 编写constexpr功能:
#if !defined(BOOST_NO_CXX11_CONSTEXPR) \
    && !defined(BOOST_NO_CXX11_HDR_ARRAY)

template <class T>
constexpr int get_size(const T& val) {
    return val.size() * sizeof(typename T::value_type);
}
  1. 如果缺少 C++ 11 特性,让我们打印一个错误:
#else
#error "This code requires C++ 11 constexpr and std::array"
#endif
  1. 就这样。现在,我们可以自由编写如下代码:
#include <array>

int main() {
    std::array<short, 5> arr;
    static_assert(get_size(arr) == 5 * sizeof(short), "");

    unsigned char data[get_size(arr)];
}

它是如何工作的...

当 C++ 11 constexpr可用时定义BOOST_NO_CXX11_CONSTEXPR宏。

constexpr关键字告诉编译器,如果该函数的所有输入都是编译时常数,则可以在编译时计算该函数。C++ 11 对constexpr函数的功能施加了很多限制。C++ 14 消除了一些限制。

当 C++ 11 std::array类和<array>头可用时,定义BOOST_NO_CXX11_HDR_ARRAY宏。

还有更多...

但是constexpr也有其他有用且有趣的宏,如下所示:

  • BOOST_CONSTEXPR宏扩展到constexpr或不扩展
  • BOOST_CONSTEXPR_OR_CONST宏扩展到constexprconst
  • BOOST_STATIC_CONSTEXPR宏与static BOOST_CONSTEXPR_OR_CONST相同

使用这些宏,可以编写利用 C++ 11 常量表达式特性的代码(如果它们可用的话):

template <class T, T Value> 
struct integral_constant { 
    BOOST_STATIC_CONSTEXPR T value = Value; 

    BOOST_CONSTEXPR operator T() const { 
        return this->value; 
    } 
}; 

现在,我们可以使用integral_constant了,如下代码所示:

char array[integral_constant<int, 10>()]; 

在示例中,调用BOOST_CONSTEXPR operator T()来获取数组大小。

C++ 11 常量表达式可以提高编译速度,并在出现错误时提供诊断信息。这是一个很好的功能。如果你的函数需要 C++ 14 中的宽松常量,那么你可以使用BOOST_CXX14_CONSTEXPR宏。只有当宽松的常量表达式可用时,它才会扩展到constexpr,否则不会扩展到任何内容。

请参见

在 C++ 11 中减少代码大小并提高用户定义类型的性能

在标准库容器中使用用户定义类型 ( UDTs )时,C++ 11 有非常具体的逻辑。只有当移动构造函数不抛出异常或没有复制构造函数时,一些容器才使用移动赋值和移动构造。

让我们看看如何确保编译器输出类move_nothrow有一个非抛出move赋值运算符和一个非抛出move构造函数。

准备好

这个食谱需要 C++ 11 数值参考的基础知识。标准库容器的知识也将很好地为您服务。

怎么做...

让我们看看如何使用 Boost 改进我们的 C++ 类。

  1. 我们只需要用BOOST_NOEXCEPT宏标记move_nothrow赋值运算符和move_nothrow构造函数:
#include <boost/config.hpp>

class move_nothrow {
    // Some class class members go here.
    // ...

public:
    move_nothrow() BOOST_NOEXCEPT;
    move_nothrow(move_nothrow&&) BOOST_NOEXCEPT
        // Members initialization goes here.
        // ...
    {}

    move_nothrow& operator=(move_nothrow&&) BOOST_NOEXCEPT {
        // Implementation goes here.
        // ...
        return *this;
    }

    move_nothrow(const move_nothrow&);
    move_nothrow& operator=(const move_nothrow&);
};
  1. 现在,我们可以在 C++ 11 中使用带有std::vector的类,无需任何修改:
#include <vector>

int main() {
    std::vector<move_nothrow> v(10);
    v.push_back(move_nothrow());
}
  1. 如果我们从move构造函数中移除BOOST_NOEXCEPT,我们将得到以下错误,因为我们没有为复制构造函数提供定义:
 undefined reference to `move_nothrow::move_nothrow(move_nothrow 
    const&)  

它是如何工作的...

在支持它的编译器上,BOOST_NOEXCEPT宏扩展到noexcept。标准库容器使用类型特征来检测构造函数是否抛出异常。类型特征主要基于noexcept说明符做出决定。

为什么没有BOOST_NOEXCEPT我们会得到一个错误?编译器的类型特性返回move_nothrow抛出的,所以std::vector尝试使用move_nothrow的复制构造函数,没有定义。

还有更多...

不管noexcept函数或方法的定义是否在单独的源文件中,BOOST_NOEXCEPT宏也会减小二进制文件的大小。

// In header file.
int foo() BOOST_NOEXCEPT; 

// In source file.
int foo() BOOST_NOEXCEPT { 
    return 0; 
} 

这是因为在后一种情况下,编译器知道函数不会抛出异常,因此没有必要生成处理它们的代码。

If a function marked as noexcept does throw an exception, your program will terminate without calling destructors for the constructed objects.

请参见

导出和导入函数和类的可移植方式

几乎所有现代语言都有能力创建具有定义良好的接口的库、类集合和方法。C++ 也不例外。我们有两种类型的库:运行时(也称为共享或动态)和静态。但是,在 C++ 中编写库并不是一项简单的任务。不同的平台有不同的方法来描述哪些符号必须从共享库中导出。

让我们看看如何使用 Boost 以可移植的方式管理符号可见性。

准备好

创建动态和静态库的经验可能对这个方法有用。

怎么做...

这个食谱的代码由两部分组成。第一部分是图书馆本身。第二部分是使用该库的代码。这两个部分使用相同的头,其中声明了库方法。使用 Boost 以可移植的方式管理符号可见性很简单,可以通过以下步骤完成:

  1. 在头文件中,我们需要来自以下头文件的定义:
#include <boost/config.hpp> 
  1. 以下代码也必须添加到头文件中:
#if defined(MY_LIBRARY_LINK_DYNAMIC)
#   if defined(MY_LIBRARY_COMPILATION)
#       define MY_LIBRARY_API BOOST_SYMBOL_EXPORT
#   else
#       define MY_LIBRARY_API BOOST_SYMBOL_IMPORT
#   endif
#else
#   define MY_LIBRARY_API
#endif
  1. 现在,所有的声明都必须使用MY_LIBRARY_API宏:
int MY_LIBRARY_API foo();

class MY_LIBRARY_API bar {
public:
    /* ... */ 
    int meow() const;
};
  1. 异常必须用BOOST_SYMBOL_VISIBLE声明;否则,只能在使用库的代码中使用catch(...)来捕获它们:
#include <stdexcept>

struct BOOST_SYMBOL_VISIBLE bar_exception
    : public std::exception 
{};
  1. 库源文件必须包括头文件:
#define MY_LIBRARY_COMPILATION
#include "my_library.hpp"
  1. 方法的定义也必须在库的源文件中:
int MY_LIBRARY_API foo() {
    // Implementation goes here.
    // ...
    return 0;
}

int bar::meow() const {
    throw bar_exception();
}
  1. 现在,我们可以使用如下代码所示的库:
#include "../06_A_my_library/my_library.hpp"
#include <cassert>

int main() {
    assert(foo() == 0);
    bar b;
    try {
        b.meow();
        assert(false);
    } catch (const bar_exception&) {}
}

它是如何工作的...

所有工作都在步骤 2 中完成。在这里,我们定义了宏MY_LIBRARY_API,它应用于我们希望从库中导出的类和方法。在步骤 2 中,我们检查MY_LIBRARY_LINK_DYNAMIC。如果没有定义,我们是在建一个静态库,没有必要定义MY_LIBRARY_API

The developer must take care of MY_LIBRARY_LINK_DYNAMIC! It will not define itself. If we are making a dynamic library, we need to make our build system to define it,

如果MY_LIBRARY_LINK_DYNAMIC被定义,我们正在构建一个运行时库,这就是变通方法的开始。作为开发人员,您必须告诉编译器,我们现在正在向用户导出函数。用户必须告诉编译器他/她正在从库中导入方法。要使导入和导出库都有一个头文件,我们使用以下代码:

#if defined(MY_LIBRARY_COMPILATION) 
#    define MY_LIBRARY_API BOOST_SYMBOL_EXPORT 
#else 
#    define MY_LIBRARY_API BOOST_SYMBOL_IMPORT 
#endif 

导出库(或者说编译库)时,必须定义MY_LIBRARY_COMPILATION。这导致MY_LIBRARY_API被定义为BOOST_SYMBOL_EXPORT。例如,见第 5 步,我们在包括my_library.hpp之前定义了MY_LIBRARY_COMPILATION。如果MY_LIBRARY_COMPILATION没有定义,表头是用户包含的,用户对那个宏一无所知。而且,如果用户包含标题,则必须从库中导入符号。

BOOST_SYMBOL_VISIBLE宏必须只用于那些没有导出但被 RTTI 使用的类。这种类的例子是异常和使用dynamic_cast进行转换的类。

还有更多...

有些编译器默认导出所有符号,但提供标志来禁用这种行为。比如 Linux 上的 GCC 和 Clang 提供-fvisibility=hidden。强烈建议使用这些标志,因为这样可以缩小二进制文件的大小,加快动态库的加载速度,并改善二进制文件的逻辑结构。当导出的符号更少时,一些过程间优化可以执行得更好。C++ 17 没有描述可见性的标准方法。希望有一天,一种可移植的可视化工作方式会出现在 C++ 中,但是在那之前,我们必须使用 Boost 中的宏。

请参见

  • 从头开始阅读本章,获取更多Boost.Config用法的示例
  • 考虑阅读Boost.Config的官方文档,了解Boost.Config宏的完整列表及其在http://boost.org/libs/config的描述

检测 Boost 版本并获取最新功能

Boost 正在积极开发中,因此每个版本都包含新的特性和库。有些人希望拥有为不同版本的 Boost 编译的库,也希望使用新版本的一些功能。

我们来看看boost::lexical_cast变更日志。根据它,Boost 1.53 有一个lexical_cast(const CharType* chars, std::size_t count)功能过载。我们的任务是在新版本的 Boost 中使用这个函数重载,在旧版本中解决这个缺失的函数重载。

准备好

只需要 C++ 和Boost.LexicalCast库的基础知识。

怎么做...

好吧,我们需要做的就是获取关于 Boost 版本的信息,并使用它来编写最佳代码。这可以按照以下步骤完成:

  1. 我们需要包括包含 Boost 版本和boost::lexical_cast的标题:
#include <boost/version.hpp>
#include <boost/lexical_cast.hpp>
  1. 如果可用,我们使用Boost.LexicalCast的新功能:
#if (BOOST_VERSION >= 105200)

int to_int(const char* str, std::size_t length) {
    return boost::lexical_cast<int>(str, length);
}
  1. 否则,我们需要先将数据复制到std::string:
#else

int to_int(const char* str, std::size_t length) {
    return boost::lexical_cast<int>(
        std::string(str, length)
    );
}

#endif
  1. 现在,我们可以使用如下所示的代码:
#include <cassert>

int main() {
    assert(to_int("10000000", 3) == 100);
}

它是如何工作的...

BOOST_VERSION宏包含以以下格式编写的 Boost 版本:一个数字代表主版本,后面是三个数字代表次版本,然后是两个数字代表补丁级别。例如,Boost 1.73.1 将包含BOOST_VERSION宏中的107301号。

所以,我们在第二步中查看 Boost 版本,根据Boost.LexicalCast的能力选择to_int功能的正确实现。

还有更多...

拥有版本宏是大型库的常见做法。某些 Boost 库允许您指定要使用的库版本;参见Boost.Thread及其BOOST_THREAD_VERSION宏示例。

顺便说一下,C++ 也有版本宏。__cplusplus宏的值允许您区分 C++ 11 之前的版本和 C++ 11,C++ 11 和 C++ 14,或者 C++ 17。目前可以定义为以下值之一:199711L201103L201402L201703L。当委员会批准标准时,宏观价值代表年和月。

请参见