首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

7.4  使用方法

为了在程序中使用variant,程序中需要包含头文件“boost/variant.hpp”。这个头文件包含了variant的全部库,所以就不需要知道要使用哪些单独的特性;以后,如果希望降低相关性,则可以只包含那些解决问题所需的相关文件。在声明variant类型时,必须定义一组它可以存储的类型。实现该要求的一个最常用的办法就是使用模板参数。一个能够存放类型为int、std::string或者double的值的variant可以声明如下。

boost::variant<int,std::string,double> my_first_variant;

在创建了变量my_first_variant之后,它就包含一个默认构造的int,这是因为int是这个variant可以存放的类型中的第一种类型。也可以通过传递一个可以转换为variant可存放类型之一的值来初始化variant。

boost::variant<int,std::string,double>

  my_first_variant("Hello world");

我们随时都可以给my_first_variant赋予新的值,只要这个新值有确定的类型,并且还可以隐式地转换为variant可以存放的类型中的某一种,它就可以很好地工作。

my_first_variant=24;

my_first_variant=2.52;

my_first_variant="Fabulous!";

my_first_variant=0;

在第一次赋值后,my_first_variant所含值的类型为int;第二次赋值后,类型变为double;第三次赋值后,类型变为std::string;最后,类型又变回int。如果想看看情况到底是不是这样的,那么可以使用函数boost::get来检索这个值,如下所示:

assert(boost::get<int>(my_first_variant)==0);

这里需要注意的是,如果get函数调用失败(当my_first_variant所含值的类型不是int时就会发生),则会抛出一个类型为boost::bad_get的异常。为了避免在失败时得到一个异常,可以将variant的指针传递给get函数,这样get函数将返回一个指向它所含值的指针,或者由于所请求的类型与variant的值的类型不匹配,这时get函数将返回一个空指针。下面是它的使用方法:

int* val=boost::get<int>(&my_first_variant);

assert(val && (*val)==0);

函数get是一种访问variant所含值的非常直接的方法,实际上它的工作方式与为boost::any提供的any_cast极为相似。这里需要注意的是,类型必须严格匹配,至少包括相同的cv限定词(const和volatile)。但是,更具有限制性的cv限定词也可以。如果类型不匹配且传递给get函数的是variant的指针,那么get函数将返回空指针。否则,抛出一个类型为bad_get的异常。

const int& i=boost::get<const int>(my_first_variant);

过分地依赖于get函数很容易使代码变得脆弱;如果不知道所含值的类型,那么可能会试探地测试所有可能的组合,如下例所示。

#include <iostream>

#include <string>

#include "boost/variant.hpp"

template <typename V> void print(V& v) {

  if (int* pi=boost::get<int>(&v))

    std::cout << "It's an int: " << *pi << '\n';

  else if (std::string* ps=boost::get<std::string>(&v))

    std::cout << "It's a std::string: " << *ps << '\n';

  else if (double* pd=boost::get<double>(&v))

    std::cout << "It's a double: " << *pd << '\n';

  std::cout << "My work here is done!\n";

}

int main() {

  boost::variant<int,std::string,double>

    my_first_variant("Hello there!");

  print(my_first_variant);

  my_first_variant=12;

  print(my_first_variant);

  my_first_variant=1.1;

  print(my_first_variant);

}

函数print现在可以正确工作,但是如果我们决定修改variant的类型集的话,那么情况又会怎么样呢?下面将在这个例子中引入一个微妙的bug,该bug在编译时无法捕获;这一次函数print将无法输出没有预先想到的任何其他类型的值。如果没有使用模板函数,而是要求一个明确的variant签名,那么就需要为不同类型的variant重载多个相同功能的函数。下一节将讨论访问variant的概念,以及这种(类型安全的)访问机制所解决的问题。

7.4.1  访问Variant

下面从一个例子开始,该例解释了为什么使用get函数后代码就没有希望的那么健壮。从上述例子的代码开始,下面更改一下variant可以包含的类型,并对variant的char值调用print函数。

int main() {

  boost::variant<int,std::string,double,char>

    my_first_variant("Hello there!");

 

  print(my_first_variant);

  my_first_variant=12;

  print(my_first_variant);

  my_first_variant=1.1;

  print(my_first_variant);

  my_first_variant='a';

  print(my_first_variant);

}

尽管上述程序给variant所包含的类型组添加了一个类型char,并且程序的最后两行设置了一个char值并调用了print函数,但是上述程序的编译过程干净利索。(这里需要注意的是,print函数是以variant类型来参数化的,所以它可以很容易适用于新的variant定义。)运行上述程序后的输出结果如下所示:

It's a std::string: Hello there!

My work here is done!

It's an int: 12

My work here is done!

It's a double: 1.1

My work here is done!

My work here is done!

上述输出结果显示出一个问题。大家注意,最后一个“My work here is done!”之前没有输出值。原因很简单,这是因为print函数不能输出除了它原来设计好的那些类型(std::string、int和double)之外的其他类型的值,但是它可以干净利索地编译和运行。如果variant的当前类型不属于print函数所支持的类型,那么就可以简单地忽略掉它的值。使用get函数还有更多潜在的问题,例如if语句的顺序要与类的层次结构相一致。这里需要注意的是,这并不是说大家应该完全避免使用get函数,这里所强调的意思是说有些时候它不是最好的解决方法。这里更好的机制应该是怎样的呢?这种机制可以允许我们声明哪些类型的值是可以接受的,并且在编译时对这些声明进行验证。这就是所谓的variant访问机制。通过把访问器应用到variant,编译器就可以保证它们完全兼容。Boost.Variant库中的这些访问器是带有函数调用运算符(function call operator)的函数对象,其中这些函数调用运算符接受与它们所访问的variant可以包含的类型集相对应的参数。

下面将这个声名狼籍的函数print重写为一个如下所示的访问器:

class print_visitor : public boost::static_visitor<void> {

public:

  void operator()(int i) const {

    std::cout << "It's an int: " << i << '\n';

  }

  void operator()(std::string s) const {

    std::cout << "It's a std::string: " << s << '\n';

  }

  void operator()(double d) const {

    std::cout << "It's a double: " << d << '\n';

  }

};

为了使print_visitor成为variant的访问器,让它公开地继承自boost::static_visitor,以获得正确的typedef (result_type),并显式地声明这个类是一个访问器类型。这个类实现了3个重载版本的函数调用运算符,分别接受int、std::string和double。为了访问variant,就需要使用函数boost::apply_visitor (visitor, variant)。下面用对apply_visitor的调用替换已有的对print的调用,其代码形式如下所示:

int main() {

  boost::variant<int,std::string,double,char>

    my_first_variant("Hello there!");

 

  print_visitor v;

 

  boost::apply_visitor(v,my_first_variant);

  my_first_variant=12;

  boost::apply_visitor(v,my_first_variant);

  my_first_variant=1.1;

  boost::apply_visitor(v,my_first_variant);

  my_first_variant='a';

  boost::apply_visitor(v,my_first_variant);

}

在上述代码中,首先创建了一个名为v的print_visitor,然后把它应用于赋值后的my_first_variant。因为没有一个函数调用运算符可以接受char的函数调用运算符,所以上述代码无法通过编译,是这样的吗?错了!因为char可以明白无误地转换为int,所以访问器与variant所包含的类型兼容。下面是上述程序运行的结果。

It's a std::string: Hello there!

It's an int: 12

It's a double: 1.1

It's an int: 97

此处我们可以学到两件事情:第一件事情就是字符a的ASCII码值为97;第二件事情,是比较重要的事情,即如果访问器是以传值的方式接受参数的,那么任意的隐式转换都能应用到所传递的值上。如果只希望使用与访问器兼容的正确类型(同时也避免复制variant的值),那么必须修改访问器函数调用运算符传递参数的方式。下面这个版本的print_visitor只能使用类型int、std::string和double,以及可以隐式转换为这些类型的引用的任何其他类型。

class print_visitor : public boost::static_visitor<void> {

public:

  void operator()(int& i) const {

    std::cout << "It's an int: " << i << '\n';

  }

  void operator()(std::string& s) const {

    std::cout << "It's a std::string: " << s << '\n';

  }

  void operator()(double& d) const {

    std::cout << "It's a double: " << d << '\n';

  }

};

如果再编译一次这个例子,编译器就真的要产生混乱,它会输出类似于下面的信息:

c:/boost_cvs/boost/boost/variant/variant.hpp:

In member function `typename Visitor::result_type boost::detail:: variant::

invoke_visitor<Visitor>::internal_visit(T&, int)

[with T = char, Visitor = print_visitor]':

[Snipped lines of irrelevant information here]

c:/boost_cvs/boost/boost/variant/variant.hpp:807:

error: no match for call to `(print_visitor) (char&)'

variant_sample1.cpp:40: error: candidates are:

  void print_visitor::operator()(int&) const

variant_sample1.cpp:44: error:

  void print_visitor::operator()(std::string&) const

variant_sample1.cpp:48: error:

  void print_visitor::operator()(double&) const

上述错误信息明确指出了问题的所在:没有一个候选的函数能够接受char参数!之所以说类型安全的编译时访问是一个功能强大的机制,这正是一个重要的原因。它使类型的访问更加健壮,并且还可以避免讨厌的类型选择(type-switching)。创建访问器与创建其他函数对象一样容易,因此这里的学习曲线并不陡峭。当variant中的类型集可能发生改变时(它们总是倾向于发生改变!),创建访问器要比仅依赖于get更加健壮。虽然开始时要付出较高的代价,但这个代价是绝对值得的。

7.4.2  泛型访问器

通过使用访问器机制和参数化函数调用运算符之后,就有可能创建能够接受任意类型(在语法上和语义上都可以处理任何泛型函数调用运算符实现所需的类型)的值的泛型访问器(generic visitor)。这对于统一地处理不同的类型非常有用。C++的运算符就是“通用”特性的一个典型例子,例如算术和IO流的位移运算符(shift operator)。下述例子使用运算符operator<<将variant的值输出到流中。

#include <iostream>

#include <sstream>

#include <string>

#include <sstream>

#include "boost/variant.hpp"

class stream_output_visitor :

  public boost::static_visitor<void> {

  std::ostream& os_;

public:

  stream_output_visitor(std::ostream& os) : os_(os) {}

  template <typename T> void operator()(T& t) const {

    os_ << t << '\n';

  }

};

int main() {

  boost::variant<int,std::string> var;

  var=100;

  boost::apply_visitor(stream_output_visitor(std::cout),var);

  var="One hundred";

  boost::apply_visitor(stream_output_visitor(std::cout),var);

}

上述例子基于以下思想,即:stream_output_visitor中的函数调用运算符的成员函数模板在访问每个类型(本例中指的是int和std::string)时都将被实例化一次。因为std::cout << 100和std::cout << std::string(“One hundred”)都已经有了定义,所以这段代码可以通过编译,并且运行结果没有错误。

当然,运算符仅是可以在泛型访问器中使用的一个例子;它们通常可以应用于许多类型。当在值上调用函数,或者将它们作为参数传递给其他函数时,就要求将那些所有类型都有的成员函数传递给运算符,并且对于被调用的函数都要有合适的重载。这种参数化函数调用运算符的另一个有趣的方面就是可以特化某些类型的行为,但对于其余类型则仍允许泛型实现。换句话说就是,可以为某些类型创建重载的函数调用运算符,而让其余类型依赖于成员函数模板。从某种意义上讲,这涉及到模板的特化,即基于类型信息对行为进行特化。

7.4.3  二元访问器

前面介绍的访问器都是一元的,也就是说它们只接受一个variant作为唯一的参数。二元访问器(binary visitor)接受两个(可能是不同的)variant。相对于其他情况来说,这个概念对于实现两个variant之间的关系非常有用。作为例子,我们将为variant类型创建一个字典式的排序顺序。为此,使用一个来自于标准库的非常有用的组件:std::ostringstream。它可以接受任意可流输出的东西,还可以根据需要生成独立的std::string。因此,可以从词汇上比较两个根本不同的variant类型,只要假设所有限定的类型都支持流输出即可。和普通的访问器一样,二元访问器也应公开地派生自boost::static_visitor,并且用模板参数表示函数调用运算符的返回类型。因为创建的是谓词,所以返回类型为bool。下面就是一个二元谓词(binary predicate),我们不久就将用到它。

class lexicographical_visitor :

  public boost::static_visitor<bool> {

public:

  template <typename LHS,typename RHS>

    bool operator()(const LHS& lhs,const RHS& rhs) const {

      return get_string(lhs)<get_string(rhs);

    }

private:

  template <typename T> static std::string

    get_string(const T& t) {

    std::ostringstream s;

    s << t;

    return s.str();

  }

  static const std::string& get_string(const std::string& s) {

     return s;

  }

};

这个二元谓词中的函数调用运算符对它的两个参数都进行了参数化,这意味着它接受任意两种类型的组合。对于variant的可用类型集的要求就是它们必须是可流输出的。成员函数模板get_string用std::ostringstream将它的参数转换为字符串表示—— 这样就可以满足可流输出的要求(为了使用std::ostringstream,请记着在程序中包含头文件<sstream>)。成员函数get_string只是负责判断类型为std::string的值是否符合要求,如果符合要求,则它会跳过std::ostringstream,直接返回它的参数。在两个参数都转换为std::string以后,剩下的工作就是使用运算符operator<来对它们进行比较。现在把这个访问器放入测试代码中,从而对一个容器中的元素进行排序(这里还将重用本章前面所创建的stream_output_visitor)。

#include <iostream>

#include <string>

#include <vector>

#include <algorithm>

#include "boost/variant.hpp"

int main() {

  boost::variant<int,std::string> var1="100";

  boost::variant<double> var2=99.99;

  std::cout << "var1<var2: " <<

    boost::apply_visitor(

      lexicographical_visitor(),var1,var2) << '\n';

  typedef std::vector<

    boost::variant<int,std::string,double> > vec_type;

  vec_type vec;

  vec.push_back("Hello");

  vec.push_back(12);

  vec.push_back(1.12);

  vec.push_back("0");

  stream_output_visitor sv(std::cout);

  std::for_each(vec.begin(),vec.end(),sv);

  lexicographical_visitor lv;

  std::sort(vec.begin(),vec.end(),boost::apply_visitor(lv));

  std::cout << '\n';

  std::for_each(vec.begin(),vec.end(),sv);

};

首先,将访问器应用于两个variant:var1和var2,如下所示:

boost::apply_visitor(lexicographical_visitor(),var1,var2)

正如大家所见,与一元访问器不同的是,有两个variant传递给了函数apply_visitor。二元访问器的一个更为常见的使用方法就是使用谓词对元素进行排序,如下所示:

lexicographical_visitor lv;

std::sort(vec.begin(),vec.end(),boost::apply_visitor(lv));

当调用sort算法时,它就会使用传递给它的谓词来比较它的元素,其中所传递的谓词是lexicographical_visitor的一个实例。这里需要注意的是,boost::variant已经定义了运算符operator<,所以不使用谓词也可以对容器进行简单排序。

std::sort(vec.begin(),vec.end());

但是这种默认的排序顺序首先使用which查看当前值的索引,因此排序后元素的排列顺序将是12、0、Hello、1.12,而我们希望的是按字典顺序进行排序。因为variant类已经提供了运算符operator<和operator==,所以variant可以用作所有标准库容器的元素类型。当这些默认的关系运算符功能不够强大时,就需要使用二元访问器来实现。

7.4.4  其他功能

本节并没有涉及到Boost.Variant库的所有功能。其他更为高级的特性没有本章已经讨论过那些特性那么常用。但是,此处对它们简要地提及一下,以便在需要时至少可以知道查找哪些可用的特性。宏BOOST_VARIANT_ENUM_PARAMS在variant类型的重载/特化函数和类模板中非常有用。这个宏用于枚举variant中可以包含的类型集。make_variant_over支持使用类型序列(也就是指表示variant的类型集的编译时列表)创建variant类型。另外还可以通过recursive_wrapper、make_recursive_variant和make_recursive_variant_over创建递归的variant类型(在创建自身就是variant类型的表达式时非常有用)。如果需要这些额外的特性,请参考variant的联机文档,其中有详细的解释。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论