C++“准”标准库Boost学习指南(3):Boost.Utility

MacOS C++开发环境配置:基于Clang与VSCode的完整指南 C++作为一门高性能的系统级编程语言,其开发环境的搭建是学习与实践的第一步。在MacOS平台上,这一过程的核心在于理解并配置其原生的LLVM/Clang工具链。Clang编译器以其快速的编译速度和清晰友好的错误信息著称,是苹果生态中C/C++开发的基石。通过配置构建任务与智能感知,开发者可以打通从编码到生成可执行程序的完整工作流,显著提升开发效率。本文聚焦于在VSCode中配置Clang编译器,详细解析tasks.json与c_cpp_properties.json等关键文件,并涵盖多文件编译、调试配置以及 阅读详情
Boost.Utility

一些本不应在一个库里出现的有用的东西,只是因为它们每个都不太复杂和广泛,不足够形成一个单独的库。但不是说它们没有什么用外;事实上小的工具通常都有最广泛的用处。在Boost, 这些小工具被集中起来,形成一个称为Utility的库。你可以在这找到checked_delete, 一个函数,用于确认在删除点的类型是完整的;还有类noncopyable,用于确保类不能被复制;还有enable_if,用于对函数重载的完全控制。

Utility 库如何改进你的程序?
  • 编译期断言 BOOST_STATIC_ASSERT
  • 安全的析构 checked_delete 和 checked_array_delete
  • 禁止复制 noncopyable
  • operator&被重载时用 addressof取得对象地址
  • 用enable_if 和 disable_if控制重载与特化

有些工具还不够组成它们自己的库,因此它们与其它实体被集合到一起。这就形成了 Boost.Utility,收集了一些没有更合适地方存放的、有用的工具。它们很有用,应该被加入到Boost,但它们又太小,不足以形成自己的库。本文介绍Boost.Utility中最基本的以及最广泛使用的工具。

我们将从 BOOST_STATIC_ASSERT开始,它是一个在编译期判断整型常量表达式的工具。然后,我们看看当你通过一个指向不完整类型的指针delete对象时,即当被删除的对象的内存布局未知时,会发生什么。checked_delete 使得这个讨论更为有趣。我们还会看到 noncopyable 如何防止一个类被复制,这也是本章最重要的主题。然后我们将看到 addressof, 它用于阻止那些重载了operator&的险恶的程序员的病态行为。最后,我们将测试 enable_if, 它非常有用,可用于在名字查找时控制函数重载与模板特化是否被考虑。


BOOST_STATIC_ASSERT

头文件: "boost/static_assert.hpp"

在运行期执行断言可能是你经常用到的,也是非常合理的。它是测试前置条件、后置条件以及不变式的好方法。执行运行期断言有很多不同的方法,但是在编译期你如何进行断言呢?当然,唯一的方法就是让编译器产生一个错误,这是很平常的事情(我在无意中都做过几千次了),但如何从错误信息中获得有意义的信息却不是那么明显的。而且,即使你在一个编译器上找到了办法,也很难把它移植到其它编译器上。这就是使用 BOOST_STATIC_ASSERT的原因。它可以在不同的平台上使用,正如我们即将看到的。

用法

要开始使用静态断言,就要包含头文件 "boost/static_assert.hpp". 该头文件定义了宏BOOST_STATIC_ASSERT. 作为它的第一个使用范例,我们来看看如何在类作用域中使用它。考虑一个泛化的类,它要求实例化时所用的类型是一个整数类型。我们不想为所有类型提供特化,因此我们需要在编译期进行测试,以确保我们的类的确是用一个整数类型进行实例化的。现在,我们先提前一点使用另一个Boost库来进行测试,它就是 Boost.Type_traits. 我们使用一个称为is_integral的断言,它对它的参数执行一个编译期求值,正如你从它的名字可以猜到的一样,求值的结果是表明该类型是否一个整数类型。

#include <iostream>

#include "boost/type_traits.hpp"
#include "boost/static_assert.hpp"

template <typename T> class only_compatible_with_integral_types {
  BOOST_STATIC_ASSERT(boost::is_integral<T>::value);
};


有了这个断言,在实例化类 only_compatible_with_integral_types 时如果试图使用一个非整型的类型,就会导致一个编译期的失败。输出信息取决于编译器,但在多数编译器下输出信息会惊人地一致。

假设我们试图这样实例化:
  1. only_compatible_with_integral_types<double> test2;
编译器将会有类似下面的输出:
Error: use of undefined type
  'boost::STATIC_ASSERTION_FAILURE<false>'

在类的作用域里,你可以明确类的要求:象在前面这样的模板中明确参数的类型就是一个明显的例子。你也可以使用断言来明确类所要求的其它前提条件,如类型的大小等等。

函数作用域中的BOOST_STATIC_ASSERT 

BOOST_STATIC_ASSERT 也可以用在函数作用域中。例如,考虑一个泛化的函数,它带有一个非类型模板参数,并且该参数只接受1至10的值。与其在运行期执行断言,我们不如在编译器使用静态断言。

template <int i> void accepts_values_between_1_and_10() {
  BOOST_STATIC_ASSERT(i>=1 && i<=10);
}


该函数的用户不能使用超出允许范围的数值来实例化这个函数。当然,断言中的表达式必须是一个纯粹的编译期表达式,也就是说,表达式中的参数和操作符都必须被编译器所认识。BOOST_STATIC_ASSERT 当然并不是只能用于泛型函数;我们可以在任何函数中很方便地测试条件。例如,一个函数需要一个与平台相关的前提条件,就常常需要一个断言。

void expects_ints_to_be_4_bytes() {
  BOOST_STATIC_ASSERT(sizeof(int)==4);
}


总结

你所看到的这种静态断言在C++中正变得象运行期断言assert那样常用。这应该至少部分地归功于"元编程革命",它使得一个程序中更多的计算量在编译期执行。表达编译期断言的唯一方法就是让编译器产生一个错误。为了让断言可用,错误提示必须可以传达有用的信息,但这很难做到可移植(事实上,根本不可能做到)。这正是 BOOST_STATIC_ASSERT 所要做的,它在大多数的编译器下提供了编译期断言的一致输出。它可用于名字空间、类、函数以及作用域。

以下情形下使用 BOOST_STATIC_ASSERT :
  • 当条件可以在编译期进行求值
  • 对类型的要求可以在编译期表示
  • 你需要对两个或以上的整型常量间的关系进行断言

checked_delete

头文件: "boost/checked_delete.hpp"

通过指针来删除一个对象时,执行的结果取决于执行删除时被删除的类型是否可知。对一个指向不完整类型的指针执行delete几乎不可能有编译器警告,这会导致各种各样的麻烦,由于析构函数可以没有被执行。换句话说,即进行清除的代码没有被执行。checked_delete 在对象析构时执行一个静态断言,测试类是否可知,以确保析构函数被执行。

用法

checked_delete 是一个boost名字空间中的模板函数。它用于删除动态分配的对象,对于动态分配的数组,同样有一个称为 checked_array_delete的模板函数。这些函数接受一个参数:要删除的指针,或是要删除的数组。这两个函数都要求在销毁对象时(即对象被传给函数时),这些被删除的类型必须是可知的。使用这些函数,要包含头文件"boost/checked_delete.hpp". 使用这些函数时,你只需象调用delete那样简单地调用它们。以下程序前向声明了一个类some_class, 而没有定义它。有些编译器允许对一个指向 some_class 的指针被删除(稍后再讨论这个),但使用 checked_delete 后,就不能通过编译了,除非有一个 some_class 的定义。

#include "boost/checked_delete.hpp"

class some_class;

some_class* create() {
  return (some_class*)0;
}

int main() {
  some_class* p=create();
  boost::checked_delete(p2);
}


如果你试图编译这段代码,对函数 checked_delete<some_class> 的实例化将失败,因为 some_class 是一个不完整的类型。你的编译器会输出类似下面的信息:
checked_delete.hpp: In function 'void
boost::checked_delete(T*) [with T = some_class]':
checked_sample.cpp:11:   instantiated from here
boost/checked_delete.hpp:34: error: invalid application of 'sizeof' to an incomplete type
boost/checked_delete.hpp:34: error: creating array with
size zero ('-1')
boost/checked_delete.hpp:35: error: invalid application of
'sizeof' to an incomplete type
boost/checked_delete.hpp:35: error: creating array with
size zero ('-1')
boost/checked_delete.hpp:32: warning: 'x' has incomplete type

错误信息的前面部分清楚地说明了问题:checked_delete 遇到了一个不完整的类型。但我们的代码中哪里存在不完整的类型呢?接下来的章节我们来讨论它。

究竟是什么问题?

在我们深入了解 checked_delete的好处之前,让我们先来彻底弄清楚问题所在。如果你试图删除一个指针,而该指针指向的是一个带有非平凡析构函数的不完整类型,结果将是未定义的行为。这是如何发生的呢?让我们来看一个例子。

// deleter.h
class to_be_deleted;

class deleter { 
public:
  void delete_it(to_be_deleted* p);
};

// deleter.cpp
#include "deleter.h"

void deleter::delete_it(to_be_deleted* p) {
  delete p;
}

// to_be_deleted.h
#include <iostream>
class to_be_deleted
{
public:
  ~to_be_deleted() {
    std::cout << 
      "I'd like to say important things here, please.";
  }
};

// Test application
#include "deleter.h"
#include "to_be_deleted.h"

int main() {
  to_be_deleted* p=new to_be_deleted;

  deleter d;
  d.delete_it(p);
}


以上代码试图 delete 一个指向不完整类型to_be_deleted的指针,这会导致未定义行为。注意,to_be_deleted 在 deleter.h中是前向声明的;deleter.cpp 包含了 deleter.h 而没有包含 to_be_deleted.h: 而to_be_deleted.h 中为to_be_deleted定义了一个非平凡析构函数。这种麻烦很容易出现,尤其是在使用智能指针的时候。我们要做的就是在调用delete时确认类型是完整的,这正是 checked_delete 所做的。

checked_delete 来解决问题

前面的例子说明了删除不完整类型时不进行确认很可能会引起麻烦,而且不是所有编译器会对此给出警告。编写泛型代码时,避免这种情况是非常必要的。使用 checked_delete重写这个例子,你只需要把 delete p 改为 checked_delete(p).

void deleter::do_it(to_be_deleted* p) {
  boost::checked_delete(p);
}
checked_delete 基本上就是一个判断类是否完整的断言,它的实现如下:
template< typename T > inline void checked_delete(T * x) {
  typedef char type_must_be_complete[sizeof(T)];
  delete x;
}


这里的想法是创建一个char的数组,数组的元素数量为T的大小。如果 checked_delete 被一个不完整的类型 T 所实例化,编译将会失败,因为 sizeof(T) 会返回 0, 而创建一个0个元素的(自动)数组是非法的。你也可以用 BOOST_STATIC_ASSERT 来执行这个断言。
  1. BOOST_STATIC_ASSERT(sizeof(T));
在编写要求使用完整类型进行实例化的模板时,这个工具非常方便。对于数组,也有一个相应的"checked deleter",称为 checked_array_delete, 它的用法类似于 checked_delete.
  1. to_be_deleted* p=new to_be_deleted[10];
  2. boost::checked_array_delete(p);
总结

删除一个动态分配的对象时,必须调用它的析构函数。如果这个类型是不完整的,即只有声明没有定义,那么析构函数可能会没被调用。这是一种潜在的危险状态,所以应该避免它。对于类模板及函数模板,风险会更大,因为无法预先知道会使用什么类型。使用 checked_delete 和 checked_array_delete, 可以解决这个删除不完整类型的问题。它没有运行期的额外开销,只是直接调用 delete, 因此说 checked_delete 带来的安全性实际上是免费的。
如果你需要在调用delete时确保类型是完整的,就使用 checked_delete 。


noncopyable

头文件: "boost/utility.hpp"

通常编译器都是程序员的好朋友,但并不总是。它的好处之一在于它会自动为我们提供复制构造函数和赋值操作符,如果我们决定不自己动手去做的话。这也可能会导致一些不愉快的惊讶,如果这个类本身就不想被复制(或被赋值)。如果真是这样,我们就需要明确地告诉这个类的使用者复制构造以及赋值是被禁止的。我不是说在代码中进行注释说明,而是说要禁止对复制构造函数以及赋值操作符的访问。幸运的是,当类带有不能复制或不能赋值的基类或成员函数时,编译器生成的复制构造函数及赋值操作符就不能使用。boost::noncopyable 的工作原理就是禁止访问它的复制构造函数和赋值操作符,然后使用它作为基类。

用法

要使用 boost::noncopyable, 你要从它私有地派生出不可复制类。虽然公有继承也可以,但这是一个坏习惯。公有继承对于阅读类声明的人而言,意味着IS-A (表示派生类IS-A 基类)关系,但表明一个类IS-A noncopyable 看起来有点不太对。要从noncopyable派生,就要包含 "boost/utility.hpp" 。

#include "boost/utility.hpp"

class please_dont_make_copies : boost::noncopyable {};

int main() {
  please_dont_make_copies d1;
  please_dont_make_copies d2(d1);
  please_dont_make_copies d3;
  d3=d1;
  }


这个例子不能通过编译。由于noncopyable的复制构造函数是私有的,因此对d2进行复制构造的尝试会失败。同样,由于noncopyable的赋值操作符也是私有的,因此将d1赋值给d3的尝试也会失败。编译器会给出类似下面的输出:
noncopyable.hpp: In copy constructor
' please_dont_make_copies::please_dont_make_copies (const please_dont_make_copies&)':
boost/noncopyable.hpp:27: error: '
  boost::noncopyable::noncopyable(const boost::noncopyable&)' is
private
noncopyable.cpp:8: error: within this context
boost/noncopyable.hpp: In member function 'please_dont_make_copies&
  please_dont_make_copies::operator=(const please_dont_make_copies&)':
boost/noncopyable.hpp:28: error: 'const boost::noncopyable&
  boost::noncopyable::operator=(const boost::noncopyable&)' is private
noncopyable.cpp:10: error: within this context

下一节我们将测试这是如何工作的。很清楚从noncopyable派生将禁止复制和赋值。这也可以通过把复制构造函数和赋值操作符定义为私有的来实现。 我们来看一下怎么样做。

使类不能复制

再看一下类 please_dont_make_copies, 为了某些原因,它不能被复制。

class please_dont_make_copies {
public:
  void do_stuff() {
    std::cout <<
      "Dear client, would you please refrain from copying me?";
  }
};


由于编译器生成了复制构造函数和赋值操作符,所以现在不能禁止类的复制和赋值。

please_dont_make_copies p1;
please_dont_make_copies p2(p1);
please_dont_make_copies p3;
p3=p2;


解决的方法是把复制构造函数和赋值操作符声明为私有的或是保护的,并增加一个缺省构造函数(因为编译器不再自动生成它了)。

class please_dont_make_copies {
public:
  please_dont_make_copies() {}

  void do_stuff() {
    std::cout << 
      "Dear client, would you please refrain from copying me?";
  }
private:
  please_dont_make_copies(const please_dont_make_copies&);
  please_dont_make_copies& operator=
    (const please_dont_make_copies&);
};


这可以很好地工作,但它不能马上清晰地告诉 please_dont_make_copies的使用者它是不能复制的。下面看一下换成 noncopyable 后,如何使得类更清楚地表明不能复制,并且也可以打更少的字。

用 noncopyable

类 boost::noncopyable 被规定为作为私有基类来使用,它可以有效地关闭复制构造和赋值操作。用前面的例子来看看使用noncopyable后代码是什么样子的:

#include "boost/utility.hpp"

class please_dont_make_copies : boost::noncopyable {
public:
  void do_stuff() {
    std::cout << "Dear client, you just cannot copy me!";
 }
};


不再需要声明复制构造函数或赋值操作符。由于我们是从noncopyable派生而来的,编译器不会再生成它们了,这样就禁止了复制和赋值。简洁可以带来清晰,尤其是象这样的基本且清楚的概念。对于阅读这段代码的使用者来说,马上就清楚地知道这个类是不能复制和赋值的,因为 boost::noncopyable 在类定义的一开始就出现了。最后要提醒的一点是:你还记得类的缺省访问控制是私有的吗?这意味着缺省上继承也是私有的。你也可以象这样写,来更加明确这个事实:
  1. class please_dont_make_copies : private boost::noncopyable {
这完全取决于观众;有些程序员认为这种多余的信息是令人讨厌并且会分散注意力,而另一些程序员则认同这种清晰性。由你来决定哪一种方法适合你的类和你的程序员。无论哪一种方法,使用 noncopyable 都要比"忘记"复制构造函数和赋值操作符的方法更加明确,也比私有地声明它们更为清晰。

记住 the Big Three

正如我们看到的那样,noncopyable 为禁止类的复制和赋值提供了一个方便的办法。但何时我们需要这样做呢?什么情况下我们需要自定义复制构造函数或赋值操作符?这个问题有一个通用的答案,一个几乎总是正确的答案:无论何时你需要定义析构函数、复制构造函数、或赋值操作符三个中的任意一个,你也需要定义另外两个。它们三者间的互动性非常重要,其中一个存在,其它的通常也都必须要有。我们假设你的一个类有一个成员是指针。你定义了一个析构函数用于正确地释放空间,但你没有定义复制构造函数和赋值操作符。这意味着你的代码中至少存在两个潜在的危险,它们很容易被触发。

class full_of_errors {
  int* value_;
public:
  full_of_errors() {
    value_=new int(13);
  }

 ~full_of_errors() {
    delete value_;
  }
};


使用这个类时,如果你忽视了编译器为这个类生成的复制构造函数和赋值操作符,那么至少有三种情况会产生错误。

full_of_errors f1;
full_of_errors f2(f1);
full_of_errors f3=f2;
full_of_errors f4;
f4=f3;


注意,第二行和第三行是调用复制构造函数的两个等价的方法。它们都会调用生成的复制构造函数,虽然语法有所不同。最后一个错误在最后一行,赋值操作符使得同一个指针被至少两个full_of_errors实例所删除。正确的方法是,我们需要自己的复制构造函数和赋值操作符,因为我们定义了我们自己的析构函数。以下是正确的方法:

  class not_full_of_errors {
    int* value_;
  public:
    not_full_of_errors() {
      value_=new int(13);
    }

    not_full_of_errors(const not_full_of_errors& other) :
      value_(new int(*other.value_)) {}

    not_full_of_errors& operator=
      (const not_full_of_errors& other) {
      *value_=*other.value_;
      return *this;
    }

    ~not_full_of_errors() {
      delete value_;
    }
};


所以,无论何时,一个类的the big three:复制构造函数、(虚拟)析构函数、和赋值操作符,中的任何一个被手工定义,在决定不需要定义其余两个之前必须认真仔细地考虑清楚。还有,如果你不想要复制,记得使用 boost::noncopyable !

总结

有很多类型需要禁止复制和赋值。但是,我们经常忽略了把这些类型的复制构造函数和赋值操作符声明为私有的,而把责任转嫁给了类的使用者。即使你使用了私有的复制构造函数和赋值操作符来确保它们不被复制或赋值,但是对于使用者而言这还不够清楚。当然,编译器会友好地提醒试图这们做的人,但错误来自何处也不是清晰的。最好我们可以清晰地做到这一点,而从 noncopyable 派生就是一个清晰的声明。当你看一眼类型的声明就可以马上知道了。编译的时候,错误信息总会包含名字 noncopyable. 而且它也节省了一些打字,这对于某些人而言是关键的因素。

以下情形下使用 noncopyable :
  • 类型的复制和赋值都不被允许
  • 复制和赋值的禁止应该尽可能明显

addressof

头文件: "boost/utility.hpp"

要取得一个变量的地址,我们要依赖于返回的值是否真的是这个变量的地址。但是,技术上重载operator&是有可能的,这意味着存有恶意的人可以破坏你的地址相关的代码。boost::addressof 被用于获得变量的地址,不管取址操作符是否被误用。通过使用一些灵巧的内部机制,模板函数 addressof 确保可以获得真实的对象及其地址。
用法

为确保获得一个对象的真实地址,你要使用 boost::addressof. 它定义在 "boost/utility.hpp". 它常用于原本要使用 operator& 的地方,它接受一个参数,该参数为要获得地址的那个对象的引用。

#include "boost/utility.hpp"

class some_class {};

int main() {
  some_class s;
  some_class* p=boost::addressof(s);
}


在进一步学习如何使用 addressof的细节前,了解一下operator&为何以及如何不一定会返回对象的地址是非常有用的。

快速了解一下存有恶意的人

如果你真的,真的,真的需要重载 operator&, 或者只是想试验一下操作符重载可能的用法,这的确很容易。当你重载 operator&时,它的语义肯定会与多数用户(以及函数!)所期望的不同,所以千万不要为了好玩而做这件事;除非有非常好的理由,否则不要去做它。以下有一段code-breaker代码:

class codebreaker {
public:
  int operator&() const {
    return 13;
  }
};


对于这个类,任何人想获取一个codebreaker实例的地址都会得到一个不可思议的数字13.

template <typename T> void print_address(const T& t) {
  std::cout << "Address: " << (&t) << '\n';
}

int main() {
  codebreaker c;
  print_address(c);
}


这不难做到,但是在实际的代码中这样做有没有好的理由?也许没有,因为除非是用在局部的类上,否则它是不安全的。原因是,虽然获取一个不完整类型的地址是合法的,但如果是要获取一个带有用户自定义operator&的不完整类型的地址则是未定义的行为。因为我们不能保证这不会发生,所以我们最好不要重载 operator&.

迅速的解决方法

即使一个类的 operator& 被重载了,也还是有办法获得这个类的实例的真实地址。addressof 使用了一些幕后的巧妙方法来获得真实的地址,而不会受任何 operator& 的欺骗。如果你把函数(print_address)改为使用 addressof, 你就可以得到以下代码:

template <typename T> void print_address(const T& t) {
  std::cout << "&t: " << (&t) << '\n';
  std::cout << "addressof(t): " << boost::addressof(t) << '\n';
}


执行时,该函数将给出如下输出(或类似于以下的输出,因为准确的地址值取决于你的系统).
&t: 13
addressof(t): 0012FECB13

差不多就是这样了!如果有什么情况让你知道或怀疑一个类的operator&被重载了,而你又需要确保得到真实的地址(由于 operator& 被重载而变得不可信了), 你就应该使用 addressof.

总结

没有多少有力的论点支持重载 operator&,但由于这是可能的,总有些人会这样做。当你编写一些需要依赖于获得对象真实地址的代码时,addressof 可以帮助你确保得到真实的地址。在编写泛型代码时,没有办法知道将会操作什么类型,因此如果需要获取参数化类型的地址的话,就使用 addressof.

当你需要获得一个对象的真实地址时,使用 addressof ,不必管 operator& 的语义。


enable_if

头文件: "boost/utility/enable_if.hpp"

有时候,我们希望控制某个函数或类模板的特化是否可以加入到重载决议时使用的重载或特化的集合中。例如,考虑一个重载的函数,它有一个版本是带一个int参数的普通函数,另一个版本是一个函数模板,它要求参数类型 T 具有一个名为type的嵌套类型。它们看起来可能象这样:

void some_func(int i) {
  std::cout << "void some_func(" << i << ")\n";
}

template <typename T> void some_func(T t) {
  typename T::type variable_of_nested_type;
  std::cout << 
    "template <typename T> void some_func(" << t << ")\n";
}


现在,想象一下当你在代码中调用 some_func 将发生什么。如果参数的类型为 int, 第一个版本将被调用。如果参数的类型是 int以外的其它类型,则第二个(模板)版本将被调用。

这没问题,只要这个类型有一个名为type的嵌套类型,但如果它没有,这段代码就不能通过编译。这会是一个问题吗?好的,考虑一下如果你用其它整数类型来调用,如short, 或 char, 或 unsigned long,那么又会发生什么。

#include <iostream>

void some_func(int i) {
  std::cout << "void some_func(" << i << ")\n";
}

template <typename T> void some_func(T t) {
  typename T::type variable_of_nested_type;
  std::cout << 
    "template <typename T> void some_func(" << t << ")\n";
}

int main() {
  int i=12;
  short s=12;

  some_func(i);
  some_func(s);
}


编译这段程序时,你将从失败的编译器中得到类似以下的输出:
enable_if_sample1.cpp: In function 'void some_func(T)
  [with T = short int]':
enable_if_sample1.cpp:17:   instantiated from here
enable_if_sample1.cpp:8: error:
  'short int' is not a class, struct, or union type

Compilation exited abnormally with code 1 at Sat Mar 06 14:30:08

就是这样。some_func 的模板版本被选为最佳的重载,但这个版本中的代码对于类型short而言是无效的。我们怎样才能避免它呢?好的,我们希望仅对含有名为type的嵌套类型的类使用模板版本的 some_func ,而对于其它没有这个嵌套类型的类则忽略它。我们能够做到。最简单的方法,但不一定是实际中总能使用的方法,是把模板版本的返回类型改为如下:

template <typename T> typename T::type* some_func(T t) {
  typename T::type variable_of_nested_type;
  std::cout <<
    "template <typename T> void some_func(" << t << ")\n";
  return 0;
}


如果你没有学过 SFINAE (匹配失败不是错误),很可能现在你的脸上会有困惑的表情。编译修改过的代码,我们的例子会通过编译。short 被提升为 int, 并且第一个版本被调用。这种令人惊奇的行为的原因是模板版本的 some_func 不再包含在重载决议的集合内了。它被排除在内是因为,编译器看到了这个函数的返回类型要求模板类型T 要有一个嵌套类型type ,而它知道 short 不满足这个要求,所以它把这个函数模板从重载决议集合中删掉了。这就是 Daveed Vandevorde 和 Nicolai Josuttis 教给我们的 SFINAE, 它意味着宁可对有问题的类型不考虑函数的重载,也不要产生一个编译器错误。如果类型有一个符合条件的嵌套类型,那么它就是重载决议集合的一部分。

class some_class {
public:
  typedef int type;
};

int main() {
  int i=12;
  short s=12;

  some_func(i);
  some_func(s);
  some_func(some_class());
}


运行该程序的输出如下:
void some_func(12)
void some_func(12)
template <typename T> void some_func(T t)

这种办法可以用,但它不太好看。在这种情形下,我们可以不管原来的 void 返回类型,我们可以用其它类型替换它。但如果不是这种情形,我们就要给函数增加一个参数并给它指定一个缺省值。

template <typename T>
  void some_func(T t,typename T::type* p=0) {
  typename T::type variable_of_nested_type;   
  std::cout << "template <typename T> void some_func(T t)\n";
}


这个版本也是使用 SFINAE 来让自己不会被无效类型所使用。这两种解决方案的问题都在于它们有点难看,我们把它们弄成了公开接口的一部分,并且它们只能在某些情形下使用。Boost 提供了一个更干净的解决方法,这种方法不仅在语法上更好看,而且提供了比前面的解决方法更多的功能。

用法

要使用 enable_if 和 disable_if, 就要包含头文件 "boost/utility/enable_if.hpp". 在第一个例子中,我们将禁止第二个版本的 some_func ,如果参数的类型是整型的话。象一个类型是否整型这样的类型信息可以用另一个Boost库Boost.Type_traits来取得。enable_if 和 disable_if 模板都通过接受一个谓词来控制是否启用或禁止一个函数。

#include <iostream>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"

void some_func(int i) {
  std::cout << "void some_func(" << i << ")\n";
}

template <typename T> void some_func(
  T t,typename boost::disable_if<
    boost::is_integral<T> >::type* p=0) {
    typename T::type variable_of_nested_type;
    std::cout << "template <typename T> void some_func(T t)\n";
}


虽然这看起来与我们前面所做的差不多,但它表达了一些我们使用直接的方法所不能表达的东西,而且它在函数的声明中表达了关于这个函数的重要信息。看到这些,我们可以清楚的知道这个函数要求类型T不能是一个整数类型。如果我们希望仅对含有嵌套类型type的类型启用这个函数,它也可以做得更好,而且我们还可以用另一个库Boost.Mpl来做。如下:

#include <iostream>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"
#include "boost/mpl/has_xxx.hpp"

BOOST_MPL_HAS_XXX_TRAIT_DEF(type)

void some_func(int i) {
  std::cout << "void some_func(" << i << ")\n";
}

template <typename T> void some_func(T t,
  typename boost::enable_if<has_type<T> >::type* p=0) {
    typename T::type variable_of_nested_type;   
    std::cout << "template <typename T> void some_func(T t)\n";
}


这真的很酷!我们现在可以对没有嵌套类型type的T禁用some_func的模板版本了,而且我们清晰地表达了这个函数的要求。这里的窍门在于使用了Boost.Mpl的一个非常漂亮的特性,它可以测试任意类型T是否内嵌有某个指定类型。通过使用宏 BOOST_MPL_HAS_XXX_TRAIT_DEF(type), 我们定义了一个名为has_type的新的trait,我们可以在函数some_func中使用它作为enable_if的谓词。如果谓词为True, 这个函数就是重载决议集合中的一员;如果谓词为 false, 这个函数就将被排除。
也可以包装返回类型,而不用增加一个额外的(缺省)参数。我们最后一个也是最好的一个 some_func, 在它的返回类型中使用 enable_if ,如下:

template <typename T> typename
boost::enable_if<has_type<T>,void>::type
  some_func(T t) {
    typename T::type variable_of_nested_type;
    std::cout << "template <typename T> void some_func(T t)\n";
}


如果你需要返回你想启用或禁用的类型,那么在返回类型中使用 enable_if 和 disable_if 会比增加一个缺省参数更合适。另外,有可能有的人真的为缺省参数指定一个值,那样就会破坏这段代码。有时,类模板的特化也需要被允许或被禁止,这时也可以使用 enable_if/disable_if 。不同的是,对于类模板,我们需要对主模板进行一些特别的处理:增加一个模板参数。考虑一个带有返回一个int的成员函数max的类模板:

template <typename T> class some_class {
public:
  int max() const {
    std::cout << "some_class::max() for the primary template\n";
    return std::numeric_limits<int>::max();
  }
};


假设我们决定对于所有算术类型(整数类型及浮点数类型), 给出一个特化版本的定义,max 返回的是该算术类型可以表示的最大值。那么我们需要对模板类型T使用std::numeric_limits,而对其它类型我们还是使用主模板。要做到这样,我们必须给主模板加一个模板参数,该参数的缺省类型为 void (这意味着用户不需要显式地给出该参数)。结果主模板的定义如下:

template <typename T,typename Enable=void> class some_class {
public:
  int max() const {
    std::cout << "some_class::max() for the primary template\n";
    return std::numeric_limits<int>::max();
  }
};


现在我们已经为提供特化版本作好了准备,该特化版本为算术类型所启用。该特性可通过 Boost.Type_traits 库获得。以下是特化版本:

template <typename T> class some_class<T,
  typename boost::enable_if<boost::is_arithmetic<T> >::type> {
public:
  T max() const {
   std::cout << "some_class::max() with an arithmetic type\n";
   return std::numeric_limits<T>::max();
  }
};


该版本只有当实例化所用的类型为算术类型时才会启用,这时特性 is_arithmetic 为 true. 它可以正常工作是因为 boost::enable_if<false>::type 是 void, 会匹配到主模板。以下程序用不同的类型测试这个模板:

#include <iostream>
#include <string>
#include <limits>
#include "boost/utility/enable_if.hpp"
#include "boost/type_traits.hpp"

// Definition of the template some_class omitted

int main() {
  std::cout << "Max for std::string: " <<
    some_class<std::string>().max() << '\n';
  std::cout << "Max for void: " << 
    some_class<void>().max() << '\n';
  std::cout << "Max for short: " << 
    some_class<short>().max() << '\n';
  std::cout << "Max for int: " << 
    some_class<int>().max() << '\n';
  std::cout << "Max for long: " << 
    some_class<long>().max() << '\n';
  std::cout << "Max for double: " << 
    some_class<double>().max() << '\n';
}


我们预期前两个 some_class 会实例化主模板,剩下的将会实例化算术类型的特化版本。运行该程序可以看到的确如此。
some_class::max() for the primary template
Max for std::string: 2147483647
some_class::max() for the primary template
Max for void: 2147483647
some_class::max() with an arithmetic type
Max for short: 32767
some_class::max() with an arithmetic type
Max for int: 2147483647
some_class::max() with an arithmetic type
Max for long: 2147483647
some_class::max() with an arithmetic type
Max for double: 1.79769e+308

一切正常!以前,要允许或禁止重载函数和模板特化需要一些编程的技巧,多数看到代码的人都不能完全明白。通过使用 enable_if 和 disable_if, 代码变得更容易写也更容易读了,并且可以从声明中自动获得正确的类型要求。在前面的例子中,我们使用了模板 enable_if, 它要求其中的条件要有一个名为value的嵌套定义。对于多数可用于元编程的类型而言这都是成立的,但对于整型常量表达式则不然。如果没有名为value的嵌套类型,就要使用 enable_if_c 来代替,它接受一个整型常量表达式。使用 is_arithmetic 并直接取出它的值,我们可以这样重写some_class的启用条件:

template <typename T> class some_class<T,
  typename boost::enable_if_c<
    boost::is_arithmetic<T>::value>::type> {
public:
  T max() const {
   std::cout << "some_class::max() with an arithmetic type\n";
   return std::numeric_limits<T>::max();
  }
};


enable_if 和 enable_if_c原则上并没有不同。它们的区别仅在于是否要求有嵌套类型value。

总结

被称为SFINAE的C++语言特性是很重要的。没有它,很多新的代码会破坏已有的代码,并且某些类型的函数重载(以及模板特化)将会无法实现。直接使用SFINAE来控制特定的函数或类型,使之被允许或被禁止用于重载决议,会很复杂。这样也会产生难以阅读的代码。使用 boost::enable_if 是更好的办法,它可以规定重载仅对某些特定类型有效。如果相同的参数用于 disable_if, 则规定重载对于符合条件的类型无效。虽然使用SFINAE也可以实现,但该库可以更好地表达相关意图。本章忽略了enable_if 和 disable_if的lazy版本(名为 lazy_enable_if 和 lazy_disable_if), 不过我在这里简单地提及一下。lazy版本被用于避免实例化类型可能无效的情形(取决于条件的取值).

以下情形时使用 enable_if :
  • 你需要在把一个符合某些条件的函数加入到或排除出重载决议集合中。
  • 你需要根据某个条件将一个类模板的特化版本加入到或排除出特化集合中。
Utility 总结

这部分我们介绍了几种工具类,它们可以大大简化我们的日常工作。BOOST_STATIC_ASSERT 提供编译期断言,它有助于我们测试前提条件或强制某些要求。对于泛型编程,checked_delete 在检查错误用法时非常有用,它可以节省我们大量的阅读可怕的错误信息和研究代码的时间。我们还讨论了 addressof, 它是一个获得对象真实地址的小工具,不用管 operator& 有否被重载。我们还看到了 enable_if 和 disable_if 如何控制某些函数参与重载决议,并学习了 SFINAE 有何意义!

我们也讨论了基类 noncopyable. 它既提供了好的习惯用法,也清楚地向任何看到这段代码的人表达了正确的意图,它值得你经常使用。总是在类中定义冗长的复制构造函数和赋值操作符,而不管它们是否需要定制,或是需要禁用,这种情况在很多代码中经常出现,它们浪费了太多的时间和金钱。

在Boost.Utility中还有一些其它的工具,我没在这里讨论它们。你可以访问Boost的网站,看看在线文档,找一下其它适合你当前工作的小工具。 http://www.boost.org/doc/libs/1_44_0/libs/utility/utility.htm

Windows下VSCode搭建C++开发环境:从编译器选型到CMake项目配置 C++作为高性能系统编程的核心语言,其开发环境的搭建是每位开发者必须掌握的基础技能。理解编译器、调试器和构建系统等工具链的协同原理,是提升工程实践能力的关键。在Windows平台上,开发者常面临在Visual Studio的“一站式”便利与轻量灵活需求间的权衡。通过VSCode这一高度可扩展的编辑器,结合MinGW-w64或MSVC等编译器工具链,可以自主搭建一个高效、可定制的C++开发环境。这种配置方式不仅让开发者深入理解从源码到可执行文件的完整构建过程,还能通过CMake实现跨平台项目的统一管理。本文聚 阅读详情

相关推荐

C++编程精髓:从核心语法到性能优化的现代工程实践指南

在计算机编程领域,内存管理和性能优化是构建高效系统的核心基础。理解内存分配、缓存机制和多线程同步等底层原理,对于开发操作系统、游戏引擎和高频交易系统等高性能应用至关重要。C++语言以其对硬件的直接控制能力和零开销抽象原则,成为实现极致性能的首选工具。通过智能指针、移动语义和RAII等现代特性,开发者可以编写更安全、高效的代码,同时利用模板元编程和编译期优化进一步提升执行效率。这些技术广泛应用于需要精细控制资源的场景,如并发网络服务器和实时数据处理系统,体现了C++在工程实践中的独特价值。

weixin_30731305的博客 426

C++:Boost库

Boost是一个功能强大,构造精良,跨越平台,代码开源,完全免费的C++程序库。共包含160余个库/组件,涵盖字符串与文本处理、容器、迭代器、算法、图像处理、模板元编程、并发编程等多个领域。由c++标准委员会成员发起倡议并建立boost社区,C++11标准库中三分之二来自boost,并且将来还会有更多的库进入c++标准库,因此boost是一个c++ "准"标准库。支持现有的所有操作系统。Boost库的大多数组件不需要编译链接,源码里直接包含头文件。(注意:包含头文件的时候需要有boost目录,即。

lingshengxiyou的博客 9571

boost::hash 基本使用(tcy)

boost::hash

tcy-阿春 3497

C++ Boost库简介

ken 671

【BOOST C++ 21 其它库】(1)库Boost.Utility

Boost.Utility 库是杂项、有用的类和函数的集合,它们太小而无法在独立库中维护。虽然实用程序很小并且可以快速学习,但它们完全无关。与其他章节中的示例不同,此处的代码示例不是相互构建的,因为它们是独立的实用程序。

gongdiwudu的专栏 4837

boost::type_traits模块用法的一些示例

boost::type_traits模块用法的一些示例实现功能C++实现代码 实现功能 boost::type_traits模块用法的一些示例 C++实现代码 #include <iostream> #include <typeinfo> #include <algorithm> #include <iterator> #include <memory> #include <boost/test/included/prg_exec_monit

希望我的博客,能帮上你解决学习中工作中所遇到的问题 617

Day1-右值引用与移动语义:从拷贝到移动的范式革命

场景做法原因返回局部对象return obj;依赖 NRVO/隐式移动,不要加std::move返回临时对象return T{};prvalue,C++17 强制消除拷贝向容器插入右值避免拷贝,直接移动容器间转移元素当元素确定不再需要时移动构造函数标记noexcept保证 STL 容器 realloc 时选择移动移动赋值运算符包含自赋值检查防止delete[]后资源丢失在类中声明析构函数同时声明移动操作避免编译器不生成默认移动操作参数按值传递调用方决定拷贝还是移动。

如风逝去 341

【BOOST C++ 15 泛型编程】(1)Boost.TypeTraits

反省是重点中的重点,泛型在实践中贯穿工程的始终。以下库支持泛型编程。无需详细了解模板元编程即可使用这些库。 Boost.TypeTraits 提供了检查类型属性的函数。 Boost.EnableIf 可以与 Boost.TypeTraits 一起使用,例如,根据函数的返回类型重载函数。 Boost.Fusion 使创建异构容器成为可能——其元素可以具有不同类型的容器。

gongdiwudu的专栏 4964

C++实战项目进阶指南:从智能指针到高并发网络编程

C++作为一门高性能系统编程语言,其核心价值在于将底层资源管理与高级抽象相结合。理解其内存模型、RAII(资源获取即初始化)等原理是掌握现代C++的关键。这些技术通过智能指针、容器等机制,有效解决了内存泄漏和资源管理难题,在游戏引擎、高频交易、分布式系统等对性能要求极高的场景中发挥着不可替代的作用。本文聚焦于C++工程实践,通过手把手实现智能指针、线程池等核心组件,深入探讨多线程同步、网络编程中的粘包拆包等实际问题,帮助开发者跨越从理论到实战的鸿沟,构建扎实的工程能力。

weixin_30696427的博客 389

C++ Boost 库中文文档

C++ BOOST库 中文文档,下载免费

boost中文手册C++

boost中文手册C++ boost库让我们更快、更好地使用C++进行大型系统或者服务器开发工作,提高工作效率,很值得一看!

ROS中用catkin+pybind11在Python调用C++类实战

在机器人开发中,Python与C++混合编程是保障算法灵活性与系统实时性的关键技术路径。其核心原理在于通过原生绑定(而非IPC或序列化)实现零拷贝、低延迟的跨语言调用,技术价值体现在显著提升ROS节点的数据吞吐与响应确定性。典型应用场景包括复用C++传感器驱动、集成Eigen/OpenCV高性能计算库,以及将实时控制逻辑无缝嵌入Python上层状态机。本文聚焦catkin构建体系下pybind11的工程化落地,覆盖从C++类封装、CMake精准配置到常见段错误/导入失败等12类高频问题的根因诊断与解决,是面

weixin_30614109的博客 423

LTspice 导入 PSpice 模型实战指南 --(解决语法兼容性与模型优化问题)

本文提供了将PSpice模型成功导入LTspice的详细实战指南。针对两者间的语法兼容性问题,文章以TI UCC23513为例,逐步讲解模型获取、符号创建、常见语法错误修改及仿真验证方法,并分享了模型优化与个人库管理技巧,帮助用户高效解决模型导入与仿真难题。

weixin_28278377的博客 817

boost库介绍以及使用

​ boost 库是一个优秀的,可移植的,开源的 C++ 库,它是由 C++ 标准委员会发起的,其中一些内容已经成为了下一代 C++ 标准库的内容,在 C++ 社区中影响甚大,是一个不折不扣的准标准库,它的功能十分强大,弥补了 C++ 很多功能函数处理上的不足。​ 学习链接: https://theboostcpplibraries.com/

huangwenhuan的专栏 1万+

AT32串口打印全攻略:从零搭建到printf重定向(GCC版)

本文详细介绍了在GCC编译环境下为AT32系列MCU实现串口打印与printf重定向的完整方案。针对从Keil MDK迁移到GCC工具链时常见的串口无输出问题,文章从环境搭建、USART配置入手,重点剖析了GCC下printf重定向的核心原理——通过重写_write()函数,并提供了经过验证的代码实现、常见问题排查及性能优化建议。

weixin_29163797的博客 263

算法-基于MACD的Adaboost股价涨跌预测模型

MACD是一个指标,具体用法是MACD>0看涨,反之看跌,果真是这样的吗?由于所有的技术指标都基于对历史数据的统计,指标的滞后性也就难免,有时候MACD明明大于0,股价仍然跌,有时候macd小于0,股价仍然涨。 本文基于Adaboost算法提出了一种基于macd的线性阀值分类器作为若分类器,通过在限定的解空间内寻找使得错误率最小的阀值和偏置,这样做避免了训练若分类器的复杂性,经验证,这种方法有效。

mtaxot的博客 8022

boost学习之-enable_if

用法: 1 你需要在把一个符合某些条件的函数加入到或排除出重载决议集合中。 你需要根据某个条件将一个类模板的特化版本加入到或排除出特化集合中。 2   实现方法,看如下例子: void some_func(int i) {       std::cout } template void some_func( T t,typename boost::

3451

KEIL-MDK代码美化神器:AStyle配置全攻略(附常用参数详解)

本文详细介绍了如何在KEIL-MDK中配置和使用AStyle代码格式化工具,包括基础参数设置、高级格式控制、团队协作方案以及KEIL深度集成技巧。通过合理的AStyle配置,开发者可以显著提升代码风格一致性,优化团队协作效率,特别适合嵌入式开发项目。

weixin_30505225的博客 146

boost.type_traits源码整理和使用说明(1)

IntroductionThe Boost type-traits library contains a set of very specific traits classes, each of which encapsulate a single trait from the C++ type system; for example, is a type a pointer or a ref

kimi's Blog 1179

ABAP CURR Data Type 深度解析与实战案例

CURR 数据类型是 SAP ABAP 中为处理货币金额而设计的一种特殊数据类型。CURR 数据类型能够自动根据关联的货币代码字段(例如 WAERS)确定小数位数,从而保证金额数据的精度与正确性。CURR 类型通常在 SAP 数据字典中使用,常见于各类业务数据表中,如会计凭证、销售订单等场景中,用于存储货币金额。CURR 数据类型底层实现上类似于 DEC 数据类型,但其特别之处在于与货币代码字段的绑定,这使得金额的格式化和显示可以自动匹配相应的货币规则。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 876

SAP Fiori Launchpad 上看不到任何 tile 应该怎么办?

中心UI组件是一个框架,为所有SAPFiori应用程序提供公共基础设施SAPFiorilaunchpad是所有SAPFioriUI的基础,并为SAPFiori应用程序提供基本功能,如登录,界面大小,应用程序之间的导航,和基于角色的应用程序目录。ABAP前端服务器包含所有基础设施组件,用于为客户端生成SAPFiori应用程序特定的UI,并与SAPBusinessSuite后端系统通信。SAPFiori应用程序的处理过程如下首先,客户端加载SAPFiori应用程序的界面。...

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 939

C/C++编程:返回类成员的引用和非引用有什么区别

#include <iostream> #include <utility> class MyClass{ public: void print_str(){ std::cout << "str: "<< str << std::endl; } void setStr(std::string Str){ str = std::move(Str); } std::strin

OceanStar的博客 912

【WorkBuddy从入门到精通实战教程】实战案例 第 41 章 论文阅读与文献综述

本文系统讲解如何用WorkBuddy高效阅读论文并撰写文献综述。针对“读了一天没记住”的痛点,提出“三遍阅读法”:快速评估相关性、结构化提炼核心内容、批判性分析局限。结合提示词工程,实现单篇精读、多篇对比与综述框架搭建,显著提升效率。强调学术诚信,严防AI幻觉,要求所有引用与数据必须回源核对。最终目标是借助AI辅助理解与组织,但坚持自主思考与写作,真正掌握科研能力。

企鹅码农的专属播客,分享10多年的实战案例和经验解读,包含YOLO、RT-DETR、AI等各个工作领域。 135

Boost的介绍、安装与环境配置

Boost的简介、下载安装以及编译、vs或vc++、vscode的环境配置

陈七的博客,学习知识分享,期待与大家的交流 5万+

从零构建爬虫抓取故宫数字文物库:Python最新实战技术详解

熟悉了动态内容加载的网页结构解析技巧。掌握了 Playwright + BeautifulSoup 的组合用法。实现了从网页→JSON→详情→图像的完整数据闭环。掌握了结构化保存与可视化分析能力。

2201_76125261的博客 1304

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

上一篇: C++“准”标准库Boost学习指南(3):Boost.Utility
下一篇: C++“准”标准库Boost学习指南(4):Boost.Operators
cbNotes
博客等级 码龄14年 295粉丝 57原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值