官术网_书友最值得收藏!

  • Learning Boost C++ Libraries
  • Arindam Mukherjee
  • 1918字
  • 2021-07-16 20:49:02

Other utilities and compile-time checks

Boost includes a number of micro-libraries that provide small but useful functionalities. Most of them are not elaborate enough to be separate libraries. Instead, they are grouped under Boost.Utility and Boost.Core. We will look at two such libraries here.

We will also look at some useful ways to detect errors as early as possible, at compile time, and glean information about the program's compilation environment and tool chains using different facilities from Boost.

BOOST_CURRENT_FUNCTION

When writing debug logs, it is incredibly useful to be able to write function names and some qualifying information about functions from where logging is invoked. This information is (obviously) available to compilers during the compilation of sources. However, the way to print it is different for different compilers. Even for a given compiler, there may be more than one ways to do it. If you want to write portable code, this is one wart you have to take care to hide. The best tool for this is the macro BOOST_CURRENT_FUNCTION, formally a part of Boost.Utility, shown in action in the following example:

Listing 2.13: Pretty printing current function name

 1 #include <boost/current_function.hpp>
 2 #include <iostream>
 3
 4 namespace FoFum {
 5 class Foo
 6 {
 7 public:
 8   void bar() {
 9     std::cout << BOOST_CURRENT_FUNCTION << '\n';
10     bar_private(5);
11   }
12
13   static void bar_static() {
14     std::cout << BOOST_CURRENT_FUNCTION << '\n';
15   }
16
17 private:
18   float bar_private(int x) const {
19     std::cout << BOOST_CURRENT_FUNCTION << '\n';
20   return 0.0;
21   }
22 };
23 } // end namespace FoFum
24
25 namespace {
26 template <typename T>
27 void baz(const T& x)
28 {
29   std::cout << BOOST_CURRENT_FUNCTION << '\n';
30 }
32 } // end unnamed namespace
33
34 int main()
35 {
36   std::cout << BOOST_CURRENT_FUNCTION << '\n';
37   FoFum::Foo f;
38   f.bar();
39   FoFum::Foo::bar_static();
40   baz(f);
41 }

Depending on your compiler, the output you see would vary in format. GNU compilers tend to have a more readable output, while on Microsoft Visual Studio you will see some very elaborate output including details such as calling conventions. In particular, the output for template instantiations is much more elaborate on Visual Studio. Here is a sample output I see on my systems.

With GNU g++:

int main()
void FoFum::Foo::bar()
float FoFum::Foo::bar1(int) const
static void FoFum::Foo::bar_static()
void {anonymous}::baz(const T&) [with T = FoFum::Foo]

With Visual Studio:

int __cdecl main(void)
void __thiscall FoFum::Foo::bar(void)
float __thiscall FoFum::Foo::bar1(int) const
void __cdecl FoFum::Foo::bar_static(void)
void __cdecl 'anonymous-namespace'::baz<class FoFum::Foo>(const class FoFum::Foo &)

You can immediately see some differences. GNU compilers call out static methods from nonstatic ones. On Visual Studio, you have to differentiate based on calling conventions (__cdecl for static member methods as well as global methods, __thiscall for instance methods). You might want to take a look at the current_function.hpp header file to figure out which macros are used behind the scenes. On GNU compilers, for example, it is __PRETTY_FUNCTION__, while on Visual Studio, it is __FUNCSIG__.

Boost.Swap

The Boost Swap library is yet another useful micro library and is part of Boost Core:

#include <boost/core/swap.hpp>
namespace boost {
  template<typename T1, typename T2>
  void swap(T1& left, T2& right);}

It wraps a well-known idiom around swapping objects. Let us first look at the problem itself to understand what is going on.

There is one global swap function in the std namespace. In many cases, for a type defined in a particular namespace, a specialized swap overload may be provided in the same namespace. When writing generic code, this can pose some challenges. Imagine a generic function that calls swap on its arguments:

 1 template <typename T>
 2 void process_values(T& arg1, T& arg2, …)
 3 {
 4   …
 5   std::swap(arg1, arg2);

In the preceding snippet, we call std::swap on line 5 to perform the swapping. While this is well-formed, this may not do what is desired in some cases. Consider the following types and functions in the namespace X:

 1 namespace X {
 2   struct Foo {};
 3
 4   void swap(Foo& left, Foo& right) { 
 5     std::cout << BOOST_CURRENT_FUNCTION << '\n';
 6   }
 7 }

Of course, X::Foo is a trivial type and X::swap is a no-op, but they can be replaced with a meaningful implementation and the points we make here would still hold.

So, what happens if you call the function process_values on two arguments of type X::Foo?

 1 X::Foo f1, f2;
 2 process_values(f1, f2, …); // calls process_values<X::Foo>

The call to process_values (line 2) will call std::swap on the passed instances of X::Foo, that is, f1 and f2. Yet, we would likely have wanted X::swap be called on f1 and f2 because it is a more appropriate overload. There is a way to do this; you call boost::swap instead. Here is the rewrite of the process_values template snippet:

 1 #include <boost/core/swap.hpp>
 2
 3 template <typename T>
 4 void process_values(T& arg1, T& arg2, …)
 5 {
 6   …
 7   boost::swap(arg1, arg2);

If you now run this code, you will see the X::swap overload printing its name to the console. To understand how boost::swap manages to call the appropriate overload, we need to understand how we could have solved this without boost::swap:

 1 template <typename T>
 2 void process_values(T& arg1, T& arg2, …)
 3 {
 4   …
 5   using std::swap;
 6   swap(arg1, arg2);

If we did not have the using declaration (line 5), the call to swap (line 6) would still have succeeded for a type T that was defined in a namespace, which had a swap overload defined for T—thanks to Argument Dependent Lookup (ADL)—X::Foo, accompanied by X::swap, is such a type. However, it would have failed for types defined in the global namespace (assuming you didn't define a generic swap in the global namespace). With the using declaration (line 5), we create the fallback for the unqualified call to swap (line 6). When ADL succeeds in finding a namespace level swap overload, the call to swap gets resolved to this overload. When ADL fails to find such an overload, then std::swap is used, as dictated, by the using declaration. The problem is that this is a nonobvious trick, and you have to know it to use it. Not every engineer in your team will come equipped with all the name lookup rules in C++. In the meantime, he can always use boost::swap, which essentially wraps this piece of code in a function. You can now use just one version of swap and expect the most appropriate overload to be invoked each time.

Compile-time asserts

Compile-time asserts require certain conditions to hold true at some point in the code. Any violation of the condition causes the compilation to fail at the point. It is an effective way to find errors at compile time, which otherwise would cause serious grief at runtime. It may also help reduce the volume and verbosity of compiler error messages of the sort generated due to template instantiation failures.

Runtime asserts are meant to corroborate the invariance of certain conditions that must hold true at some point in the code. Such a condition might be the result of the logic or algorithm used or could be based on some documented convention. For example, if you are writing a function to raise a number to some power, how do you handle the mathematically undefined case of both the number and the power being zero? You can use an assert to express this explicitly, as shown in the following snippet (line 6):

 1 #include <cassert>
 2
 3 double power(double base, double exponent)
 4 {
 5   // no negative powers of zero
 6   assert(base != 0 || exponent > 0);
 7   …
 8 }

Any violation of such invariants indicates a bug or a flaw, which needs to be fixed, and causes a catastrophic failure of the program in debug builds. Boost provides a macro called BOOST_STATIC_ASSERT that takes an expression, which can be evaluated at compile time and triggers a compilation failure if this expression evaluates to false.

For example, you may have designed a memory allocator class template that is meant to be used only with "small" objects. Of course, smallness is arbitrary, but you can design your allocator to be optimized for objects of size 16 bytes or smaller. If you want to enforce correct usage of your class, you should simply prevent its instantiation for any class of size greater than 16 bytes. Here is our first example of BOOST_STATIC_ASSERT that helps you enforce the small object semantics of your allocator:

Listing 2.16a: Using compile-time asserts

 1 #include <boost/static_assert.hpp>
 2
 3 template <typename T>
 4 class SmallObjectAllocator
 5 {
 6   BOOST_STATIC_ASSERT(sizeof(T) <= 16);
 7
 8 public:
 9   SmallObjectAllocator() {}
10 };

We define our dummy allocator template called SmallObjectAllocator (lines 3 and 4) and call the BOOST_STATIC_ASSERT macro in the class scope (line 6). We pass an expression to the macro that must be possible to evaluate at compile time. Now, sizeof expressions are always evaluated by the compiler and 16 is an integer literal, so the expression sizeof(T) <= 16 can be entirely evaluated at compile time and can be passed to BOOST_STATIC_ASSERT. If we now instantiate the SmallObjectAllocator with a type Foo, whose size is 32 bytes, we will get a compiler error due to the static assert on line 6. Here is the code that can trigger the assertion:

Listing 2.16b: Using compile-time asserts

11 struct Foo
12 {
13   char data[32];
14 };
15
16 int main()
17 {
18   SmallObjectAllocator<int> intAlloc;
19   SmallObjectAllocator<Foo> fooAlloc; // ERROR: sizeof(Foo) > 16
20 }

We define a type Foo whose size is 32 bytes, which is larger than the maximum supported by SmallObjectAllocator (line 13). We instantiate the SmallObjectAllocator template with the types int (line 18) and Foo (line 19) . The compilation fails for SmallObjectAllocator<Foo>, and we get an error message.

Tip

C++11 supports compile-time asserts using the new static_assert keyword. If you are using a C++11 compiler, BOOST_STATIC_ASSERT internally uses static_assert.

The actual error message naturally varies from compiler to compiler, especially on C++03 compilers. On C++11 compilers, because this internally uses the static_assert keyword, the error message tends to be more uniform and meaningful. However, on pre-C++11 compilers too, you get a fairly accurate idea of the offending line. On my system, using the GNU g++ compiler in C++03 mode, I get the following errors:

StaticAssertTest.cpp: In instantiation of 'class SmallObjectAllocator<Foo>':
StaticAssertTest.cpp:19:29:   required from here
StaticAssertTest.cpp:6:3: error: invalid application of 'sizeof' to incomplete type 'boost::STATIC_ASSERTION_FAILURE<false>'

The last line of the compiler error refers to an incomplete type boost::STATIC_ASSERTION_FAILURE<false>, which comes from the innards of the BOOST_STATIC_ASSERT macro. It is clear that there was an error on line 6, and the static assertion failed. If I switch to C++11 mode, the error messages are a lot saner:

StaticAssertTest.cpp: In instantiation of 'class SmallObjectAllocator<Foo>':
StaticAssertTest.cpp:19:29:   required from here
StaticAssertTest.cpp:6:3: error: static assertion failed: sizeof(T) <= 16

There is another variant of the static assert macro called BOOST_STATIC_ASSERT, which takes a message string as the second parameter. With C++11 compilers, it simply prints this message for the error message. Under pre-C++11 compilers, this message may or may not make it to the compiler error content. You use it this way:

 1 BOOST_STATIC_ASSERT_MSG(sizeof(T) <= 16, "Objects of size more" 
 2                         " than 16 bytes not supported.");

Not all expressions can be evaluated at compile time. Mostly, expressions involving constant integers, sizes of types, and general type computations can be evaluated at compile time. The Boost TypeTraits library and the Boost Metaprogramming Library (MPL) offer several metafunctions using which many sophisticated conditions can be checked on types at compile time. We illustrate such use with a small example. We will see more examples of such use in later chapters.

We may use static assertions not only in class scope but also in function and namespace scope. Here is an example of a library of function templates that allow bitwise operations on different POD types. When instantiating these functions, we assert at compile time that the types passed are POD types:

Listing 2.17: Using compile-time asserts

 1 #include <boost/static_assert.hpp>
 2 #include <boost/type_traits.hpp>
 3
 4 template <typename T, typename U>
 5 T bitwise_or (const T& left, const U& right)
 6 {
 7 BOOST_STATIC_ASSERT(boost::is_pod<T>::value && 
 8 boost::is_pod<U>::value);
 9 BOOST_STATIC_ASSERT(sizeof(T) >= sizeof(U));
10
11   T result = left;
12   unsigned char *right_array =
13           reinterpret_cast<unsigned char*>(&right);
14   unsigned char *left_array =
15           reinterpret_cast<unsigned char*>(&result);
16   for (size_t i = 0; i < sizeof(U); ++i) {
17     left_array[i] |= right_array[i];
18   }
19
20   return result;
21 }

Here, we define a function bitwise_or (lines 4 and 5) , which takes two objects, potentially of different types and sizes, and returns the bitwise-or of their content. Inside this function, we use the metafunction boost::is_pod<T> to assert that both the objects passed are of POD types (line 7). Also, because the return type of the function is T, the type of the left argument, we assert that the function must always be called with the larger argument first (line 9) so that there is no data loss.

Diagnostics using preprocessor macros

A number of times in my career as a software engineer, I have worked on products with a single code base that were built on five different flavors of Unix and on Windows, often in parallel. Often these build servers would be big iron servers with hundreds of gigs of attached storage that would be used by multiple products for the purpose of building. There would be myriad environments, tool chains, and configurations cohabiting on the same server. It must have taken ages to stabilize these systems to a point where everything built perfectly. One day, all hell broke loose when, overnight, without any significant check-ins having gone in, our software started acting weird. It took us almost a day to figure out that someone had tinkered with the environment variables, as a result of which we were linking using a different version of the compiler and linking with a different runtime from the one with which our third-party libraries were built. I don't need to tell you that this was not ideal for a build system even at the time that it existed. Unfortunately, you may still find such messed up environments that take a long time to set up and then get undone by a flippant change. What saved us that day after half a day's fruitless toil was the good sense of using preprocessor macros to dump information about the build system, including compiler names, versions, architecture, and their likes at program startup. We could soon glean enough information from this data dumped by the program, before it inevitably crashed and we spotted the compiler mismatch.

Such information is doubly useful for library writers who might be able to provide the most optimal implementation of a library on each compiler or platform by leveraging specific interfaces and doing conditional compilation of code based on preprocessor macro definitions. The bane of working with such macros is, however, the absolute disparity between different compilers, platforms, and environments on how they are named and what their function is. Boost provides a much more uniform set of preprocessor macros for gleaning information about the software build environment through its Config and Predef libraries. We will look at a handful of useful macros from these libraries.

The Predef library is a header-only library that provides all sorts of macros for getting useful information about the build environment at compile time. The information available can fall into different categories. Rather than providing a long list of options and explaining what they do—a job that the online documentation does adequately—we will look at the following code to illustrate how this information is accessed and used:

Listing 2.18a: Using diagnostic macros from Predef

 1 #include <boost/predef.h>
 2 #include <iostream>
 3
 4 void checkOs()
 5 {
 6   // identify OS
 7 #if defined(BOOST_OS_WINDOWS)
 8   std::cout << "Windows" << '\n';
 9 #elif defined(BOOST_OS_LINUX)
10   std::cout << "Linux" << '\n';
11 #elif defined(BOOST_OS_MACOS)
12   std::cout << "MacOS" << '\n';
13 #elif defined(BOOST_OS_UNIX)
14   std::cout << Another UNIX" << '\n'; // *_AIX, *_HPUX, etc. 
15 #endif
16 }

The preceding function uses the BOOST_OS_* macros from the Predef library to identify the OS on which the code is built. We have only shown macros for three different OSes. The online documentation provides a full list of macros for identifying different OSes.

Listing 2.18b: Using diagnostic macros from Predef

 1 #include <boost/predef.h>
 2 #include <iostream>
 34 void checkArch()
 5 {
 6   // identify architecture
 7 #if defined(BOOST_ARCH_X86)
 8 #if defined(BOOST_ARCH_X86_64)
 9   std::cout << "x86-64 bit" << '\n';
10  #else
11   std::cout << "x86-32 bit" << '\n';
12  #endif
13 #elif defined(BOOST_ARCH_ARM)
14   std::cout << "ARM" << '\n';
15 #else
16   std::cout << "Other architecture" << '\n';
17 #endif
18 }

The preceding function uses the BOOST_ARCH_* macros from the Predef library to identify the architecture of the platform on which the code is built. We have only shown macros for x86 and ARM architectures; the online documentation provides a complete list of macros for identifying different architectures.

Listing 2.18c: Using diagnostic macros from Predef

 1 #include <boost/predef.h>
 2 #include <iostream>
 3
 4 void checkCompiler()
 5 {
 6   // identify compiler
 7 #if defined(BOOST_COMP_GNUC)
 8   std::cout << "GCC, Version: " << BOOST_COMP_GNUC << '\n';
 9 #elif defined(BOOST_COMP_MSVC)
10   std::cout << "MSVC, Version: " << BOOST_COMP_MSVC << '\n';
11 #else
12   std::cout << "Other compiler" << '\n';
13 #endif
14 }

The preceding function uses the BOOST_COMP_* macros from the Predef library to identify the compiler that was used to build the code. We have only shown macros for GNU and Microsoft Visual C++ compilers. The online documentation provides a complete list of macros for identifying different compilers. When defined, the BOOST_COMP_* macro for a particular compiler evaluates to its numeric version. For example, on Visual Studio 2010, BOOST_COMP_MSVC evaluates to 100030319. This could be translated as version 10.0.30319:

Listing 2.18d: Using diagnostic macros from Predef

 1 #include <boost/predef.h>
 2 #include <iostream>
 3
 4 void checkCpp11()
 5 {
 6   // Do version checks
 7 #if defined(BOOST_COMP_GNUC)
 8  #if BOOST_COMP_GNUC < BOOST_VERSION_NUMBER(4, 8, 1)
 9    std::cout << "Incomplete C++ 11 support" << '\n';
10  #else
11    std::cout << "Most C++ 11 features supported" << '\n';
12  #endif
13 #elif defined(BOOST_COMP_MSVC)
14 #if BOOST_COMP_MSVC < BOOST_VERSION_NUMBER(12, 0, 0)
15    std::cout << "Incomplete C++ 11 support" << '\n';
16  #else
17    std::cout << "Most C++ 11 features supported" << '\n';
18  #endif
19 #endif
20 }

In the preceding code, we use the BOOST_VERSION_NUMBER macro to construct versions against which we compare the current version of the GNU or Microsoft Visual C++ compilers. If the GNU compiler version is less than 4.8.1 or the Microsoft Visual Studio C++ compiler version is less than 12.0, we print that the support for C++11 might be incomplete.

In the final example of this section, we use macros from boost/config.hpp to print compiler, platform, and runtime library names (lines 6, 7, and 8). We also use two macros defined in boost/version.hpp to print the version of Boost used, as a string (line 10) and as a numeric value (line 11):

Listing 2.19: Using configuration information macros

 1 #include <boost/config.hpp>
 2 #include <boost/version.hpp>
 3 #include <iostream>
 4 
 5 void buildEnvInfo() {
 6 std::cout << "Compiler: " << BOOST_COMPILER << '\n'
 7 << "Platform: " << BOOST_PLATFORM << '\n'
 8 << "Library: " << BOOST_STDLIB << '\n';
 9
10 std::cout << "Boost version: " << BOOST_LIB_VERSION << '['
11 << BOOST_VERSION << ']' << '\n';
12 }
主站蜘蛛池模板: 论坛| 龙井市| 江津市| 安阳县| 沐川县| 云霄县| 博白县| 登封市| 寻乌县| 霍林郭勒市| 来安县| 西城区| 通河县| 和政县| 长宁县| 连云港市| 安平县| 托克逊县| 芦山县| 德清县| 泗阳县| 肥乡县| 潢川县| 临猗县| 利川市| 石林| 天长市| 乌兰县| 会东县| 台南县| 铜鼓县| 北流市| 南投县| 五原县| 兴化市| 临沂市| 翼城县| 东丰县| 金寨县| 昌乐县| 新安县|