在本章中,我们将介绍:
- 检测操作系统和编译器
- 检测 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++ 的基础知识。
食谱很简单,由一个标题和一个宏组成。
- 标题:
#include <boost/predef/compiler.h>
- 宏:
#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
库,那是很有秩序的,稍微不那么漂亮,但是功能要多得多
一些编译器支持扩展算术类型,如 128 位浮点或整数。让我们快速浏览一下如何使用 Boost 来使用它们。
我们将创建一个接受三个参数并返回这些方法的乘积值的方法。如果编译器支持 128 位整数,那么我们就使用它们。如果编译器支持long long
,那么我们就用它;否则,我们需要发出编译时错误。
只需要 C++ 的基础知识。
我们需要什么来处理 128 位整数?显示它们可用的宏和一些跨平台具有可移植类型名称的typedefs
。
- 包括标题:
#include <boost/config.hpp>
- 现在,我们需要检测 int128 支持:
#ifdef BOOST_HAS_INT128
- 增加一些
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;
}
- 对于不支持 int128 类型且没有
long long
的编译器,我们可能会产生编译时错误:
#else // #ifdef BOOST_HAS_INT128
#ifdef BOOST_NO_LONG_LONG
#error "This code requires at least int64_t support"
#endif
- 现在,我们需要使用
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 int
和unsigned long long int
内置类型支持 64 位类型。不幸的是,并不是所有的编译器都支持 C++ 11,所以BOOST_NO_LONG_LONG
可能对你有用。
128 位整数不是 C++ 17 的一部分,所以typedefs
和宏来自 Boost 是编写可移植代码的方式之一。
C++ 标准化委员会正在进行一项关于添加编译时指定宽度的整数的工作。当这项工作完成时,您将能够创建 128 位、512 位甚至 8388608 位(1 MB 大)整数。
- 有关
Boost.Config
的更多信息,请阅读食谱检测并绕过禁用的 RTTI 。 - 阅读【http://boost.org/libs/config】的`Boost.Config`官方文档,了解更多关于其能力的信息。
- Boost 中有一个库,允许构造无限精度的类型。跟着链接http://boost.org/libs/multiprecision去看看
Boost.Multiprecision
图书馆。
一些公司和库对他们的 C++ 代码有特定的要求,比如没有 RTTI 的成功编译。
在这个小食谱中,我们不仅要检测禁用的 RTTI,还要从头开始编写一个类似 Boost 的库,存储关于类型的信息,并在运行时比较类型,即使没有typeid
。
这个食谱需要 C++ RTTI 用法的基本知识。
检测禁用的 RTTI、存储关于类型的信息以及在运行时比较类型是在 Boost 库中广泛使用的技巧。
- 为此,我们首先需要包含以下标题:
#include <boost/config.hpp>
- 让我们首先看看启用了 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);
}
- 否则,我们需要构建自己的
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);
}
- 最后一步是定义
type_id
功能:
template <class T>
inline type_index type_id() {
return type_index(BOOST_CURRENT_FUNCTION);
}
#endif
- 现在,我们可以比较类型:
#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
也尽量显示函数参数,包括模板参数。
- 阅读即将到来的食谱,了解更多关于
Boost.Config
的信息 - 浏览http://github.com/boostorg/type_index查看
Boost.TypeIndex
库的源代码 - 在http://boost.org/libs/config阅读
Boost.Config
的官方文件 - 在 http://boost.org/libs/type_index 阅读
Boost.TypeIndex
图书馆的官方文件 - 食谱获取人类可读的类型名称在第 01 章、开始编写您的应用将向您介绍
Boost.TypeIndex
的一些其他功能
第 4 章、编译时技巧和第 8 章、元编程都致力于元编程。如果你试图使用那些章节中的技巧,你可能已经注意到写一个元功能会花费很多时间。因此,在编写一个可移植的实现之前,使用更用户友好的方法来尝试元功能可能是一个好主意,比如 C++ 11 constexpr
。
在这个食谱中,我们来看看如何检测constexpr
支持。
constexpr
函数是可以在编译时计算的函数。对于这个食谱,我们只需要知道这些。
让我们看看如何检测编译器对constexpr
特性的支持:
- 就像本章的其他食谱一样,我们从以下标题开始:
#include <boost/config.hpp>
- 编写
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);
}
- 如果缺少 C++ 11 特性,让我们打印一个错误:
#else
#error "This code requires C++ 11 constexpr and std::array"
#endif
- 就这样。现在,我们可以自由编写如下代码:
#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
宏扩展到constexpr
或const
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
,否则不会扩展到任何内容。
- 更多关于
constexpr
用法的信息可以在http://en.cppreference.com/w/cpp/language/constexpr阅读 - 阅读
Boost.Config
官方文档,了解更多关于http://boost.org/libs/config宏的信息
在标准库容器中使用用户定义类型 ( UDTs )时,C++ 11 有非常具体的逻辑。只有当移动构造函数不抛出异常或没有复制构造函数时,一些容器才使用移动赋值和移动构造。
让我们看看如何确保编译器输出类move_nothrow
有一个非抛出move
赋值运算符和一个非抛出move
构造函数。
这个食谱需要 C++ 11 数值参考的基础知识。标准库容器的知识也将很好地为您服务。
让我们看看如何使用 Boost 改进我们的 C++ 类。
- 我们只需要用
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&);
};
- 现在,我们可以在 C++ 11 中使用带有
std::vector
的类,无需任何修改:
#include <vector>
int main() {
std::vector<move_nothrow> v(10);
v.push_back(move_nothrow());
}
- 如果我们从
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.
- 描述为什么允许
move
构造函数抛出异常以及容器必须如何移动对象的文档可在http://www . open-STD . org/JT C1/sc22/wg21/docs/papers/2010/n 3050 . html上获得 - 阅读
Boost.Config
的官方文档,了解更多BOOST_NOEXCEPT
的示例,例如http://boost.org/libs/config的 Boost 中存在的宏
几乎所有现代语言都有能力创建具有定义良好的接口的库、类集合和方法。C++ 也不例外。我们有两种类型的库:运行时(也称为共享或动态)和静态。但是,在 C++ 中编写库并不是一项简单的任务。不同的平台有不同的方法来描述哪些符号必须从共享库中导出。
让我们看看如何使用 Boost 以可移植的方式管理符号可见性。
创建动态和静态库的经验可能对这个方法有用。
这个食谱的代码由两部分组成。第一部分是图书馆本身。第二部分是使用该库的代码。这两个部分使用相同的头,其中声明了库方法。使用 Boost 以可移植的方式管理符号可见性很简单,可以通过以下步骤完成:
- 在头文件中,我们需要来自以下头文件的定义:
#include <boost/config.hpp>
- 以下代码也必须添加到头文件中:
#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
- 现在,所有的声明都必须使用
MY_LIBRARY_API
宏:
int MY_LIBRARY_API foo();
class MY_LIBRARY_API bar {
public:
/* ... */
int meow() const;
};
- 异常必须用
BOOST_SYMBOL_VISIBLE
声明;否则,只能在使用库的代码中使用catch(...)
来捕获它们:
#include <stdexcept>
struct BOOST_SYMBOL_VISIBLE bar_exception
: public std::exception
{};
- 库源文件必须包括头文件:
#define MY_LIBRARY_COMPILATION
#include "my_library.hpp"
- 方法的定义也必须在库的源文件中:
int MY_LIBRARY_API foo() {
// Implementation goes here.
// ...
return 0;
}
int bar::meow() const {
throw bar_exception();
}
- 现在,我们可以使用如下代码所示的库:
#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::lexical_cast
变更日志。根据它,Boost 1.53 有一个lexical_cast(const CharType* chars, std::size_t count)
功能过载。我们的任务是在新版本的 Boost 中使用这个函数重载,在旧版本中解决这个缺失的函数重载。
只需要 C++ 和Boost.LexicalCast
库的基础知识。
好吧,我们需要做的就是获取关于 Boost 版本的信息,并使用它来编写最佳代码。这可以按照以下步骤完成:
- 我们需要包括包含 Boost 版本和
boost::lexical_cast
的标题:
#include <boost/version.hpp>
#include <boost/lexical_cast.hpp>
- 如果可用,我们使用
Boost.LexicalCast
的新功能:
#if (BOOST_VERSION >= 105200)
int to_int(const char* str, std::size_t length) {
return boost::lexical_cast<int>(str, length);
}
- 否则,我们需要先将数据复制到
std::string
:
#else
int to_int(const char* str, std::size_t length) {
return boost::lexical_cast<int>(
std::string(str, length)
);
}
#endif
- 现在,我们可以使用如下所示的代码:
#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。目前可以定义为以下值之一:199711L
、201103L
、201402L
或201703L
。当委员会批准标准时,宏观价值代表年和月。
- 阅读第五章、多线程中的创建执行线程的配方,了解更多关于
BOOST_THREAD_VERSION
及其如何影响Boost.Thread
库的信息,或者阅读http://boost.org/libs/thread的文档 - 从头开始读本章或者考虑在http://boost.org/libs/config阅读
Boost.Config
的官方文件