摘要 | 使用此库编写可以通过名称接受参数的函数和类模板 |
---|
new_window( "alert" , _width=10 , _titlebar=false ); smart_ptr< Foo , deleter<Deallocate<Foo> > , copy_policy<DeepCopy> > p(new Foo);
由于命名参数可以按任何顺序传递,因此当函数或模板具有多个具有有用默认值的参数时,它们特别有用。该库还支持推导参数:也就是说,可以从其类型推导出身份的参数。
作者 | David Abrahams, Daniel Wallin |
---|---|
联系方式 | dave@boost-consulting.com, daniel@boostpro.com |
组织 | BoostPro Computing |
版权 | Copyright David Abrahams, Daniel Wallin 2005-2009。在 Boost 软件许可协议 1.0 版下发布。(请参见随附的 LICENSE_1_0.txt 文件或复制于 https://boost.ac.cn/LICENSE_1_0.txt) |
[注意:本教程未涵盖该库的所有详细信息。另请参阅参考文档]
在 C++ 中,参数通常通过它们相对于参数列表的位置来赋予含义:传递的第一个参数映射到函数定义中的第一个参数,依此类推。 当最多只有一个具有默认值的参数时,该协议很好,但是当甚至有几个有用的默认值时,位置接口会变得繁琐
由于参数的含义由其位置给出,因此我们必须为具有默认值的参数选择一个(通常是任意的)顺序,从而使某些默认值的组合不可用
window* new_window( char const* name , int border_width = default_border_width , bool movable = true , bool initially_visible = true ); bool const movability = false; window* w = new_window("alert box", movability);
在上面的示例中,我们想要创建一个具有默认值的不可移动窗口border_width,但相反,我们得到了一个具有border_width为零的可移动窗口。 为了获得所需的效果,我们需要编写
window* w = new_window( "alert box", default_border_width, movability );
读者可能很难理解调用站点参数的含义
window* w = new_window("alert", 1, true, false);
这个窗口是可移动且最初不可见的,还是不可移动且最初可见的? 读者需要记住参数的顺序才能确定。
调用的作者也可能不记得参数的顺序,从而导致难以发现的错误。
该库通过将每个参数名称与关键字对象关联来解决上述问题。 现在,用户可以通过名称而不是位置来识别参数
window* w = new_window( "alert box" , movable_=false ); // OK!
推导参数可以在不提供显式参数名称的情况下,在任何位置传递。 函数具有可以根据传递的参数类型唯一标识的参数并不少见。name参数到new_window就是一个这样的例子。 如果有效,则其他任何参数都不能合理地转换为char const*。 使用推导参数接口,我们可以在任何参数位置传递窗口名称,而不会引起歧义
window* w = new_window( movable_=false , "alert box" ); // OK! window* w = new_window( "alert box" , movable_=false ); // OK!
如果使用得当,推导参数接口可以使使用者免于记住形式参数名称的负担。
我们给出的命名和推导参数接口的理由同样适用于类模板,也适用于函数。 使用参数库,我们可以创建允许显式命名模板参数的接口(在本例中为shared和Client),像这样
smart_ptr< ownership<shared> , value_type<Client> > p;
传递命名模板参数的语法不如函数参数的语法那么自然(理想情况下,我们应该能够编写smart_ptr<ownership = shared, …>)。 当与类模板一起使用时,这种小的语法缺陷使推导参数成为一个特别大的优势
// p and q could be equivalent, given a deduced // parameter interface. smart_ptr<shared, Client> p; smart_ptr<Client, shared> q;
本教程介绍了所有基础知识——如何为函数模板和类模板构建命名参数和推导参数接口——以及几种更高级的用法。
在本节中,我们将展示如何使用参数库为 Boost 图形库的depth_first_search算法构建一个富有表现力的接口。1
参数库的大多数组件都在以组件命名的头文件中声明。 例如,
#include <boost/parameter/keyword.hpp>
将确保boost::parameter::keyword对编译器已知。 还有一个组合头文件,boost/parameter.hpp,它包含库的大多数组件。 对于本教程的其余部分,除非我们另有说明,否则可以使用上面的规则来确定要#include访问库的任何给定组件。
此外,下面的示例也将像命名空间别名一样编写
namespace parameter = boost::parameter;
已声明:我们将编写parameter::xxx而不是boost::parameter::xxx.
的抽象接口depth_first_search图形库的
template < typename Graph , typename DFSVisitor , typename Index , typename ColorMap > void depth_first_search( Graph const& graph , DFSVisitor visitor , typename graph_traits<g>::vertex_descriptor root_vertex , IndexMap index_map , ColorMap& color );
但是,大多数参数都有一个有用的默认值,如下表所示。
参数名称 | 数据流 | 类型 | 默认值(如果有) |
---|---|---|---|
graph | 输入 | 关联图和顶点列表图的模型 | 无 - 此参数是必需的。 |
visitor | 输入 | DFS 访问者的模型 | boost::dfs_visitor<>() |
root_vertex | 输入 | graph's 顶点描述符类型。 | *vertices(graph).first |
index_map | 输入 | 可读属性映射的模型,其键类型 :=graph's 顶点描述符,其值类型为整数类型。 | get(boost::vertex_index,graph) |
color_map | 输入/输出 | 读/写属性映射的模型,其键类型 :=graph's 顶点描述符类型。 | aboost::iterator_property_map从一个std::vector的default_color_type的大小num_vertices(graph)并使用index_map作为索引映射。 |
不要被上面第二列和第三列中的信息吓倒。 为了本次练习的目的,您无需详细了解它们。
本次练习的目的是使调用depth_first_search使用命名参数成为可能,省略任何默认值合适的参数
graphs::depth_first_search(g, color_map_=my_color_map);
为了使该语法合法,需要存在一个名为“color_map_”的对象,该对象的赋值运算符可以接受一个my_color_map参数。 在此步骤中,我们将为每个参数创建一个这样的 关键字对象。 每个关键字对象将由唯一的 关键字标签类型 标识。
我们将在命名空间graphs中定义我们的接口。 该库提供了一个方便的宏来定义关键字对象
#include <boost/parameter/name.hpp> namespace graphs { BOOST_PARAMETER_NAME(graph) // Note: no semicolon BOOST_PARAMETER_NAME(visitor) BOOST_PARAMETER_NAME(root_vertex) BOOST_PARAMETER_NAME(index_map) BOOST_PARAMETER_NAME(color_map) }
您在此处看到的graphkeyword 的声明等效于
namespace graphs { namespace tag { // keyword tag type struct graph { typedef boost::parameter::forward_reference qualifier; }; } namespace // unnamed { // A reference to the keyword object boost::parameter::keyword<tag::graph> const& _graph = boost::parameter::keyword<tag::graph>::instance; } }
它定义了一个名为tag::graph的关键字标签类型和一个名为_graph.
的关键字对象引用。 涉及未命名命名空间和引用的这种“花式舞蹈”都是为了避免在使用函数模板(在多个翻译单元中实例化)时违反单一定义规则 (ODR)2(MSVC6.x 用户请参阅此说明)。
现在我们已经定义了关键字,函数模板定义遵循一个简单的模式,使用BOOST_PARAMETER_FUNCTION宏
#include <boost/parameter/preprocessor.hpp> namespace graphs { BOOST_PARAMETER_FUNCTION( (void), // 1. parenthesized return type depth_first_search, // 2. name of the function template tag, // 3. namespace of tag types (required (graph, *) ) // 4. one required parameter, and (optional // four optional parameters, // with defaults (visitor, *, boost::dfs_visitor<>()) (root_vertex, *, *vertices(graph).first) (index_map, *, get(boost::vertex_index,graph)) (color_map, *, default_color_map(num_vertices(graph), index_map) ) ) ) { // ... body of function goes here... // use graph, visitor, index_map, and color_map } }
到BOOST_PARAMETER_FUNCTION的参数是
函数签名被描述为一个或两个相邻的带括号的术语(一个 Boost.Preprocessor 序列),描述了函数参数的顺序,这些参数将按照位置传递的顺序排列。任何必需的参数必须首先出现,但是(required … )子句可以在所有参数都是可选的情况下省略。
必需参数首先给出——嵌套在(required … )子句中——作为一系列描述每个参数名称和对参数类型的任何要求的二元组。 在这种情况下,只有一个必需参数,所以只有一个元组
(required (graph, *) )
由于depth_first_search对其graph参数没有任何特定类型的要求,我们使用星号来表示允许任何类型。 必需参数必须始终位于签名中的任何可选参数之前,但如果没有必需参数,则(required … )子句可以完全省略。
可选参数——嵌套在(optional … )子句中——给出为一系列相邻的三元组,描述参数名称、对参数类型的任何要求,以及表示参数默认值的表达式
(optional (visitor, *, boost::dfs_visitor<>()) (root_vertex, *, *vertices(graph).first) (index_map, *, get(boost::vertex_index,graph)) (color_map, *, default_color_map(num_vertices(graph), index_map) ) )
默认情况下,Boost.Parameter 将所有参数视为 forward 参数,函数将通过右值引用接收这些参数,并且仅使用std::forward或boost::forward传递给其他函数。 这些参数可以是const左值、可变左值、const右值或可变右值。 因此,默认配置为用户代码提供了最大的灵活性。 然而
BOOST_PARAMETER_NAME(graph) BOOST_PARAMETER_NAME(visitor) BOOST_PARAMETER_NAME(in(root_vertex)) BOOST_PARAMETER_NAME(in(index_map)) BOOST_PARAMETER_NAME(in_out(color_map))
中。 为了了解当参数绑定到违反其类别约束的参数时会发生什么,请尝试使用LIBS_PARAMETER_TEST_COMPILE_FAILURE_0宏或LIBS_PARAMETER_TEST_COMPILE_FAILURE_1宏#defined来编译 compose.cpp 测试程序。 您应该会遇到由特定约束冲突引起的编译器错误。
当参数按位置传递(不使用关键字)时,它们将按照参数在签名中给出的顺序映射到参数,因此例如在以下调用中
graphs::depth_first_search(x, y);
x将始终被解释为图,而y将始终被解释为访问器。
请注意,在我们的示例中,图参数的值用于root_vertex, index_map和color_map.
(required (graph, *) ) (optional (visitor, *, boost::dfs_visitor<>()) (root_vertex, *, *vertices(graph).first) (index_map, *, get(boost::vertex_index, graph)) (color_map, *, default_color_map(num_vertices(graph), index_map) ) )
的默认表达式中。 如果为该参数传递了实际参数,则永远不会对默认表达式进行求值——甚至不会被实例化。 实际上,我们可以通过用打印参数的内容替换depth_first_search的主体来证明这一点
#include <boost/graph/depth_first_search.hpp> // for dfs_visitor BOOST_PARAMETER_FUNCTION( (bool), depth_first_search, tag …signature goes here… ) { std::cout << "graph=" << graph; std::cout << std::endl; std::cout << "visitor=" << visitor; std::cout << std::endl; std::cout << "root_vertex=" << root_vertex; std::cout << std::endl; std::cout << "index_map=" << index_map; std::cout << std::endl; std::cout << "color_map=" << color_map; std::cout << std::endl; return true; } #include <boost/core/lightweight_test.hpp> int main() { char const* g = "1"; depth_first_search(1, 2, 3, 4, 5); depth_first_search( g, '2', _color_map = '5', _index_map = "4", _root_vertex = "3" ); return boost::report_errors(); }
尽管诸如vertices(graph).first之类的默认表达式对于给定的graph参数来说是不合法的,但两个调用都将编译,并且每个调用将打印完全相同的内容。
事实上,函数签名非常通用,以至于任何少于五个参数的depth_first_search调用都将匹配我们的函数,前提是我们为必需的graph参数传递了某些内容。 乍一看这似乎不是问题; 毕竟,如果参数与depth_first_search的实现施加的要求不匹配,则稍后在其主体实例化时会出现编译错误。
非常通用的函数签名至少存在三个问题。
通常,当传递的参数类型不合适时,最好阻止函数被考虑用于重载解析。 当未提供所需的graph参数时,该库已经这样做了,但我们不太可能看到不接受图进行操作的深度优先搜索。 假设,相反,我们发现了一种不同的深度优先搜索算法,该算法可以在不模拟 关联图 的图上工作? 如果我们只是添加一个简单的重载,那将是模棱两可的
// new overload BOOST_PARAMETER_FUNCTION((void), depth_first_search, (tag), (required (graph,*))( … ) ) { // new algorithm implementation } … // ambiguous! depth_first_search(boost::adjacency_list<>(), 2, "hello");
我们真的不希望编译器考虑原始版本的depth_first_search,因为root_vertex参数,"hello",不满足 requirement,即它与graph参数的顶点描述符类型匹配。 相反,此调用应仅调用我们的新重载。 为了将原始的depth_first_search重载移出竞争范围,我们首先按如下方式编码此要求
struct vertex_descriptor_predicate { template <typename T, typename Args> struct apply : boost::mpl::if_< boost::is_convertible< T , typename boost::graph_traits< typename boost::parameter::value_type< Args , graphs::graph >::type >::vertex_descriptor > , boost::mpl::true_ , boost::mpl::false_ > { }; };
此编码是 MPL 二进制元函数类,一种具有嵌套的apply元函数的类型,该函数接收两个模板参数。 对于第一个模板参数,Boost.Parameter 将传入我们将施加要求的类型。 对于第二个模板参数,Boost.Parameter 将传入整个参数包,从而可以通过depth_first_searchvalue_type元函数和相应的关键字标记类型来提取每个其他参数的类型。 结果类型的apply元函数将等效于boost::mpl::true_如果T满足我们由boost::is_convertible语句施加的要求; 否则,结果将等效于boost::mpl::false_.
此时,我们可以将元函数类的名称附加到函数签名的相应*元素,用括号括起来。
(root_vertex , *(vertex_descriptor_predicate) , *vertices(graph).first )
现在,只有当depth_first_search参数可以转换为图的顶点描述符类型时,才会调用原始的root_vertex,并且我们曾经模棱两可的示例将顺利地调用新的重载。
我们可以使用相同的概念对其他参数的要求进行编码; 只需要为每个参数调整嵌套apply元函数的实现即可。 这里没有空间给出图形库细节的完整描述,但足以表明接下来的几个元函数类提供了必要的检查。
struct graph_predicate { template <typename T, typename Args> struct apply : boost::mpl::eval_if< boost::is_convertible< typename boost::graph_traits<T>::traversal_category , boost::incidence_graph_tag > , boost::mpl::if_< boost::is_convertible< typename boost::graph_traits<T>::traversal_category , boost::vertex_list_graph_tag > , boost::mpl::true_ , boost::mpl::false_ > > { }; }; struct index_map_predicate { template <typename T, typename Args> struct apply : boost::mpl::eval_if< boost::is_integral< typename boost::property_traits<T>::value_type > , boost::mpl::if_< boost::is_same< typename boost::property_traits<T>::key_type , typename boost::graph_traits< typename boost::parameter::value_type< Args , graphs::graph >::type >::vertex_descriptor > , boost::mpl::true_ , boost::mpl::false_ > > { }; }; struct color_map_predicate { template <typename T, typename Args> struct apply : boost::mpl::if_< boost::is_same< typename boost::property_traits<T>::key_type , typename boost::graph_traits< typename boost::parameter::value_type< Args , graphs::graph >::type >::vertex_descriptor > , boost::mpl::true_ , boost::mpl::false_ > { }; };
同样,计算color_map参数的默认值并非易事,因此最好将计算分解为单独的函数模板。
template <typename Size, typename IndexMap> boost::iterator_property_map< std::vector<boost::default_color_type>::iterator , IndexMap , boost::default_color_type , boost::default_color_type& >& default_color_map(Size num_vertices, IndexMap const& index_map) { static std::vector<boost::default_color_type> colors(num_vertices); static boost::iterator_property_map< std::vector<boost::default_color_type>::iterator , IndexMap , boost::default_color_type , boost::default_color_type& > m(colors.begin(), index_map); return m; }
签名将每个谓词元函数括在括号中并在前面加上星号,如下所示
BOOST_PARAMETER_FUNCTION((void), depth_first_search, graphs, (required (graph, *(graph_predicate)) ) (optional (visitor , * // not easily checkable , boost::dfs_visitor<>() ) (root_vertex , (vertex_descriptor_predicate) , *vertices(graph).first ) (index_map , *(index_map_predicate) , get(boost::vertex_index, graph) ) (color_map , *(color_map_predicate) , default_color_map(num_vertices(graph), index_map) ) ) )
通常没有必要如此完全地编码泛型函数参数的类型要求。 但是,这样做是值得的:您的代码将更易于自我记录,并且通常会提供更好的用户体验。 通过 对约束和概念的语言支持,您还将更轻松地过渡到 C++20 标准。
将类型要求编码到函数的参数上对于使函数能够具有推导的参数接口至关重要。 让我们重新审视一下new_window示例
window* w = new_window( movable_=false , "alert box" ); window* w = new_window( "alert box" , movable_=false );
这次的目标是能够在不指定关键字的情况下调用new_window函数。 对于每个具有所需类型的参数,我们可以将该类型括在括号中,然后替换参数签名的*元素
BOOST_PARAMETER_NAME((name_, keywords) name) BOOST_PARAMETER_NAME((movable_, keywords) movable) BOOST_PARAMETER_FUNCTION((window*), new_window, keywords, (deduced (required (name, (char const*)) (movable, (bool)) ) ) ) { // ... }
以下语句现在可以工作,并且彼此等效,也与先前的语句等效
window* w = new_window(false, "alert box"); window* w = new_window("alert box", false);
为了进一步说明推导参数的支持,请考虑来自 Boost.Python 的 def 函数的示例。它的签名大致如下:
template < typename Function , typename KeywordExpression , typename CallPolicies > void def( // Required parameters char const* name, Function func // Optional, deduced parameters , char const* docstring = "" , KeywordExpression keywords = no_keywords() , CallPolicies policies = default_call_policies() );
请尽量不要被本示例中“关键字”一词的使用分散注意力:尽管它在 Boost.Python 中的含义与在 Parameter 库中的含义类似,但就本练习而言,您可以认为它们是完全不同的。
当调用def时,只需要两个参数。任何附加参数与其参数之间的关联可以通过实际传递的参数类型来确定,因此调用者既不需要记住参数位置,也不需要为这些参数显式指定参数名称。要使用以下方法生成此接口:BOOST_PARAMETER_FUNCTION,我们只需要将推导的参数括在(deduced …)子句中,如下所示:
char const*& blank_char_ptr() { static char const* larr = ""; return larr; } BOOST_PARAMETER_FUNCTION( (bool), def, tag, (required (name, (char const*)) (func,*) ) // nondeduced (deduced (optional (docstring, (char const*), "") (keywords // see5 , *(is_keyword_expression<boost::mpl::_>) , no_keywords() ) (policies , *( boost::mpl::eval_if< boost::is_convertible<boost::mpl::_,char const*> , boost::mpl::false_ , boost::mpl::if_< // see5 is_keyword_expression<boost::mpl::_> , boost::mpl::false_ , boost::mpl::true_ > > ) , default_call_policies() ) ) ) ) { … }
语法说明
一个(deduced …)子句始终包含一个(required …)和/或一个(optional …)子句,并且必须遵循在外部级别指示非推导参数的任何(required …)或(optional …)子句。
通过以上声明,以下两个调用是等效的:
char const* f_name = "f"; def( f_name , &f , some_policies , "Documentation for f" ); def( f_name , &f , "Documentation for f" , some_policies );
如果用户想要传递一个policies参数,该参数出于某种原因也可以转换为char const*,她总是可以显式指定参数名称,如下所示:
def( f_name , &f , _policies = some_policies , "Documentation for f" );
deduced.cpp 和 deduced_dependent_predicate.cpp 测试程序演示了推导参数支持的更多用法。
对于某些算法,返回类型取决于至少一个参数类型。但是,在使用以下方法编码此返回类型时,需要避免一个陷阱:BOOST_PARAMETER_FUNCTION或其他代码生成宏。例如,给定以下定义:
BOOST_PARAMETER_NAME(x) BOOST_PARAMETER_NAME(y) BOOST_PARAMETER_NAME(z)
让我们的算法简单地返回它的一个参数。如果我们天真地根据parameter::value_type:
BOOST_PARAMETER_FUNCTION( (typename parameter::value_type<Args,tag::y>::type), return_y, tag, (deduced (required (x, (std::map<char const*,std::string>)) (y, (char const*)) ) (optional (z, (int), 4) ) ) ) { return y; }
实现其返回类型,那么使用return_y以任何方式使用,除了使用位置参数之外,都会导致编译器错误
std::map<char const*,std::string> k2s; assert("foo" == return_y(2, k2s, "foo")); // error!
问题是即使y是一个必需参数,BOOST_PARAMETER_FUNCTION也会生成某些重载,其中参数包类型Args实际上不包含关键字标签类型tag::y。解决方案是使用 SFINAE 来排除这些重载的生成。由于parameter::value_type是一个元函数,因此我们的工具是lazy_enable_if:
BOOST_PARAMETER_FUNCTION( ( // Whenever using 'enable_if', 'disable_if', and so on, // do not add the 'typename' keyword in front. boost::lazy_enable_if< typename mpl::has_key<Args,tag::y>::type , parameter::value_type<Args,tag::y> > // Whenever using 'enable_if', 'disable_if', and so on, // do not add '::type' here. ), return_y, tag, (deduced (required (x, (std::map<char const*,std::string>)) (y, (char const*)) ) (optional (z, (int), 4) ) ) ) { return y; }
有关工作演示,请参见 preprocessor_deduced.cpp。
以下宏:BOOST_PARAMETER_MEMBER_FUNCTION和BOOST_PARAMETER_CONST_MEMBER_FUNCTION接受与BOOST_PARAMETER_FUNCTION完全相同的参数,但设计为在类的正文中使用
BOOST_PARAMETER_NAME(arg1) BOOST_PARAMETER_NAME(arg2) struct callable2 { BOOST_PARAMETER_CONST_MEMBER_FUNCTION( (void), call, tag, (required (arg1,(int))(arg2,(int))) ) { std::cout << arg1 << ", " << arg2; std::cout << std::endl; } }; #include <boost/core/lightweight_test.hpp> int main() { callable2 c2; callable2 const& c2_const = c2; c2_const.call(1, 2); return boost::report_errors(); }
这些宏不直接允许将函数的接口与其实现分离,但您始终可以将参数转发到单独的实现函数
struct callable2 { BOOST_PARAMETER_CONST_MEMBER_FUNCTION( (void), call, tag, (required (arg1,(int))(arg2,(int))) ) { call_impl(arg1, arg2); } private: void call_impl(int, int); // implemented elsewhere. };
要公开静态成员函数,只需在函数名称前插入关键字“static”即可
BOOST_PARAMETER_NAME(arg1) struct somebody { BOOST_PARAMETER_MEMBER_FUNCTION( (void), static f, tag, (optional (arg1,(int),0)) ) { std::cout << arg1 << std::endl; } }; #include <boost/core/lightweight_test.hpp> int main() { somebody::f(); somebody::f(4); return boost::report_errors(); }
以下宏:BOOST_PARAMETER_FUNCTION_CALL_OPERATOR和BOOST_PARAMETER_CONST_FUNCTION_CALL_OPERATOR宏接受与BOOST_PARAMETER_MEMBER_FUNCTION和BOOST_PARAMETER_CONST_MEMBER_FUNCTION宏相同的参数,除了函数名称之外,因为这些宏允许将封闭类的实例视为函数对象
BOOST_PARAMETER_NAME(first_arg) BOOST_PARAMETER_NAME(second_arg) struct callable2 { BOOST_PARAMETER_CONST_FUNCTION_CALL_OPERATOR( (void), tag, (required (first_arg,(int))(second_arg,(int))) ) { std::cout << first_arg << ", "; std::cout << second_arg << std::endl; } }; #include <boost/core/lightweight_test.hpp> int main() { callable2 c2; callable2 const& c2_const = c2; c2_const(1, 2); return boost::report_errors(); }
C++ 中缺少“委托构造函数”功能(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n1986.pdf)在某种程度上限制了此库可以为定义启用参数的构造函数提供的接口质量。构造函数委托的通常解决方法适用:必须将公共逻辑分解为一个或多个基类。
让我们构建一个启用参数的构造函数,该构造函数仅打印其参数。第一步是编写一个基类,其构造函数接受一个名为 ArgumentPack 的参数:一个包含对实际参数的引用的捆绑包,并用它们的关键字标记。实际参数的值通过使用关键字对象索引它从 ArgumentPack 中提取
BOOST_PARAMETER_NAME(name) BOOST_PARAMETER_NAME(index) struct myclass_impl { template <typename ArgumentPack> myclass_impl(ArgumentPack const& args) { std::cout << "name = " << args[_name]; std::cout << "; index = " << args[_index | 42]; std::cout << std::endl; } };
请注意,按位或(“|”)运算符在应用于传递给 ArgumentPack 的索引运算符的关键字对象时,具有特殊含义:它用于指示默认值。在这种情况下,如果 ArgumentPack 中没有index参数,则将使用42代替。
现在我们准备编写启用参数的构造函数接口
struct myclass : myclass_impl { BOOST_PARAMETER_CONSTRUCTOR( myclass, (myclass_impl), tag , (required (name,*)) (optional (index,*)) ) // no semicolon };
由于我们已为index提供默认值,但未为name提供默认值,因此仅name是必需的。我们可以按如下方式使用我们的新接口
myclass x("bob", 3); // positional myclass y(_index = 12, _name = "sally"); // named myclass z("june"); // positional/defaulted
有关 ArgumentPack 操作的更多信息,请参见高级主题部分。
在本节中,我们将使用 Boost.Parameter 来构建 Boost.Python 的 class_ 模板,其“签名”为
template < ValueType, BaseList = bases<> , HeldType = ValueType, Copyable = void > class class_;
只有第一个参数,ValueType是必需的。
首先,我们将构建一个允许用户按位置或按名称传递参数的接口
struct B { virtual ~B() = 0; }; struct D : B { ~D(); }; class_< class_type<B> , copyable<boost::noncopyable> > …; class_< D , held_type<std::auto_ptr<D> > , base_list<bases<B> > > …;
第一步是为每个模板参数定义关键字
namespace boost { namespace python { BOOST_PARAMETER_TEMPLATE_KEYWORD(class_type) BOOST_PARAMETER_TEMPLATE_KEYWORD(base_list) BOOST_PARAMETER_TEMPLATE_KEYWORD(held_type) BOOST_PARAMETER_TEMPLATE_KEYWORD(copyable) }}
您在此处看到的class_typekeyword 的声明等效于
namespace boost { namespace python { namespace tag { struct class_type; // keyword tag type } template <typename T> struct class_type : parameter::template_keyword<tag::class_type,T> { }; }}
它定义了一个名为tag::class_type的关键字标签类型和一个名为class_type.
2.5.1.2 类模板框架下一步是定义类模板的框架,该框架具有三个可选参数。由于用户可以按任何顺序传递参数,因此我们不知道这些参数的实际身份,因此使用描述性名称或为其中任何一个编写实际默认值还为时过早。相反,我们将为它们提供通用名称,并使用特殊类型boost::parameter::void_
namespace boost { namespace python { template < typename A0 , typename A1 = boost::parameter::void_ , typename A2 = boost::parameter::void_ , typename A3 = boost::parameter::void_ > struct class_ { … }; }}
2.5.1.3 类模板签名接下来,我们需要构建一个类型,称为 ParameterSpec,描述了boost::python::class_
namespace boost { namespace python { using boost::mpl::_; typedef parameter::parameters< required<tag::class_type, boost::is_class<_> > , parameter::optional<tag::base_list, mpl::is_sequence<_> > , parameter::optional<tag::held_type> , parameter::optional<tag::copyable> > class_signature; }}
2.5.1.4 参数包和参数提取接下来,在class_的正文中,我们使用 ParameterSpec 的嵌套::bind< … >模板将实际参数捆绑到 ArgumentPack 类型中,然后使用库的value_type< … >模板将实际参数捆绑到 ArgumentPack 类型中,然后使用库的元函数来提取“逻辑参数”。很像binding< … >
namespace boost { namespace python { template < typename A0 , typename A1 = boost::parameter::void_ , typename A2 = boost::parameter::void_ , typename A3 = boost::parameter::void_ > struct class_ { // Create ArgumentPack typedef typename class_signature::template bind< A0, A1, A2, A3 >::type args; // Extract first logical parameter. typedef typename parameter::value_type< args, tag::class_type >::type class_type; typedef typename parameter::value_type< args, tag::base_list, bases<> >::type base_list; typedef typename parameter::value_type< args, tag::held_type, class_type >::type held_type; typedef typename parameter::value_type< args, tag::copyable, void >::type copyable; }; }}
2.5.2 练习到目前为止的代码
typedef boost::python::class_< class_type<B>, copyable<boost::noncopyable> > c1; typedef boost::python::class_< D , held_type<std::auto_ptr<D> > , base_list<bases<B> > > c2;
重新访问我们的原始示例,
BOOST_MPL_ASSERT((boost::is_same<c1::class_type, B>)); BOOST_MPL_ASSERT((boost::is_same<c1::base_list, bases<> >)); BOOST_MPL_ASSERT((boost::is_same<c1::held_type, B>)); BOOST_MPL_ASSERT(( boost::is_same<c1::copyable, boost::noncopyable> )); BOOST_MPL_ASSERT((boost::is_same<c2::class_type, D>)); BOOST_MPL_ASSERT((boost::is_same<c2::base_list, bases<B> >)); BOOST_MPL_ASSERT(( boost::is_same<c2::held_type, std::auto_ptr<D> > )); BOOST_MPL_ASSERT((boost::is_same<c2::copyable, void>));
2.5.3 推导的模板参数要在此处应用推导参数接口,我们只需要使类型要求稍微严格一些,以便和held_typecopyable参数可以与其它参数清晰地区分。Boost.Python 通过要求base_list是其bases< … >held_type模板的特化(而不是任何旧的 MPL 序列),并且通过要求(如果显式提供)为boost::noncopyable是其。识别
namespace boost { namespace python { namespace detail { struct bases_base { }; } template < typename A0 = void, typename A1 = void, typename A2 = void … > struct bases : detail::bases_base { }; }}
特化的一个简单方法是从同一个类派生它们,作为一个实现细节
typedef parameter::parameters< required<tag::class_type, is_class<_> > , parameter::optional< deduced<tag::base_list> , is_base_and_derived<detail::bases_base,_> > , parameter::optional< deduced<tag::held_type> , mpl::not_< mpl::or_< is_base_and_derived<detail::bases_base,_> , is_same<noncopyable,_> > > > , parameter::optional< deduced<tag::copyable> , is_same<noncopyable,_> > > class_signature;
现在我们可以重写我们的签名,以使所有三个可选参数都可以推导
typedef boost::python::class_<B, boost::noncopyable> c1; typedef boost::python::class_< D, std::auto_ptr<D>, bases<B> > c2;
看起来我们增加了很多复杂性,但是对我们用户的益处更大。现在,无需显式参数名称即可编写我们的原始示例
至此,您应该对基础知识有很好的掌握。在本节中,我们将介绍该库的一些更深奥的用法。如果您不喜欢用于引用关键字对象的前导下划线命名约定,或者您需要tag这个名称用于关键字类型命名空间之外的其它内容,那么还有另一种使用:
BOOST_PARAMETER_NAME( ( object-name , tag-namespace ) parameter-name )
BOOST_PARAMETER_NAME
BOOST_PARAMETER_NAME( ( pass_foo, keywords ) foo ) BOOST_PARAMETER_FUNCTION( (int), f, keywords, (required (foo, *)) ) { return foo + 1; } int x = f(pass_foo = 41);
的方法。这是一个用法示例
但是,在使用此更冗长的形式之前,请阅读有关 关键字对象命名的最佳实践的部分。
3.2.1 构建 ArgumentPack
BOOST_PARAMETER_NAME(index) template <typename ArgumentPack> int print_index(ArgumentPack const& args) { std::cout << "index = " << args[_index]; std::cout << std::endl; return 0; } int x = print_index(_index = 3); // prints "index = 3"
最简单的 ArgumentPack 是赋值给关键字对象的结果此外,可以使用逗号运算符组合 ArgumentPack。以下额外的括号用于防止编译器看到:
BOOST_PARAMETER_NAME(name) template <typename ArgumentPack> int print_name_and_index(ArgumentPack const& args) { std::cout << "name = " << args[_name]; std::cout << "; "; return print_index(args); } int y = print_name_and_index((_index = 3, _name = "jones"));
print_name_and_index
的两个单独的参数。compose.cpp 测试程序显示了此功能的更多示例。的正文中,我们使用 ParameterSpec 的嵌套要构建带有位置参数的 ArgumentPack,我们可以使用 ParameterSpec。正如在关于类模板签名的部分中介绍的,ParameterSpec 描述了参数的位置顺序和任何关联的类型要求。正如我们可以使用其嵌套
parameter::parameters< required<tag::name, is_convertible<_,char const*> > , optional<tag::index, is_convertible<_,int> > > spec; char const sam[] = "sam"; int twelve = 12; int z0 = print_name_and_index( spec( sam, twelve ) ); int z1 = print_name_and_index( spec( _index=12, _name="sam" ) );
3.2.2 提取参数类型此外,可以使用逗号运算符组合 ArgumentPack。以下额外的括号用于防止编译器看到如果我们想知道传递给
BOOST_PARAMETER_NAME(name) BOOST_PARAMETER_NAME(index) template <typename Name, typename Index> int deduce_arg_types_impl(Name&& name, Index&& index) { // we know the types Name&& n2 = boost::forward<Name>(name); Index&& i2 = boost::forward<Index>(index); return index; } template <typename ArgumentPack> int deduce_arg_types(ArgumentPack const& args) { return deduce_arg_types_impl(args[_name], args[_index | 42]); }
的参数类型,我们有几个选择。最简单且不易出错的方法是将它们转发到函数模板并允许它执行类型推导index有时需要推导参数类型而无需额外的函数调用层。例如,假设我们想返回模板将实际参数捆绑到 ArgumentPack 类型中,然后使用库的参数的两倍值?在这种情况下,我们可以使用 前面介绍的
BOOST_PARAMETER_NAME(index) template <typename ArgumentPack> typename boost::parameter::value_type<ArgumentPack,tag::index,int>::type twice_index(ArgumentPack const& args) { return 2 * args[_index | 42]; }
元函数很像请注意,如果我们使用模板将实际参数捆绑到 ArgumentPack 类型中,然后使用库的而不是,我们将最终返回对在2 * …
3.2.3 延迟默认计算BOOST_PARAMETER_FUNCTION当默认值的计算成本很高时,最好避免它,直到我们确定它绝对必要。可以为我们解决该问题,但是当显式使用 ArgumentPack 时,我们需要:
BOOST_PARAMETER_NAME(s1) BOOST_PARAMETER_NAME(s2) BOOST_PARAMETER_NAME(s3) template <typename ArgumentPack> std::string f(ArgumentPack const& args) { std::string const& s1 = args[_s1]; std::string const& s2 = args[_s2]; typename parameter::binding< ArgumentPack,tag::s3,std::string >::type s3 = args[_s3 | (s1 + s2)]; // always constructs s1 + s2 return s3; } std::string x = f(( _s1="hello,", _s2=" world", _s3="hi world" ));
运算符 |以外的工具。在上面的示例中,字符串"hello, world"的构造,尽管用户向我们传递了s3的值。为了解决这个问题,我们可以使用
typename parameter::binding< ArgumentPack,tag::s3,std::string >::type s3 = args[ _s3 || boost::bind( std::plus<std::string>(), boost::ref(s1), boost::ref(s2) ) ];
表达式bind(std::plus<std::string>(), ref(s1), ref(s2))生成一个函数对象,当调用它时,会将两个字符串连接在一起。该函数仅当调用者没有"hello, world"提供参数时才会被调用。
到目前为止,你应该对如何使用 Parameter 库有相当好的理解了。本节指出了一些更边缘的问题,这些问题将帮助你更有效地使用该库。
这个名称用于关键字类型命名空间之外的其它内容,那么还有另一种使用在前导到我们所有关键字对象的名称中添加一个下划线,以避免以下通常是静默的错误
namespace people { namespace tag { struct name { typedef boost::parameter::forward_reference qualifier; }; struct age { typedef boost::parameter::forward_reference qualifier; }; } namespace // unnamed { boost::parameter::keyword<tag::name>& name = boost::parameter::keyword<tag::name>::instance; boost::parameter::keyword<tag::age>& age = boost::parameter::keyword<tag::age>::instance; } BOOST_PARAMETER_FUNCTION( (void), g, tag, (optional (name, *, "bob")(age, *, 42)) ) { std::cout << name << ":" << age; } void f(int age) { :vellipsis:` . . . ` g(age = 3); // whoops! } }
虽然在上面的例子中,用户试图传递值3作为age参数到g,但实际发生的是时,才会评估它的第二个参数。类似地,在'sage参数被重新赋值为 3,然后作为位置参数传递给g。因为g's 的第一个位置参数是name,所以age使用 的默认值,并且 g 打印3:42。我们的前导下划线命名约定使得这个问题不太可能发生。
在这种特殊情况下,如果 f 的age参数已被设置为const,就可以检测到该问题,这在可能的情况下始终是一个好主意。最后,我们建议你为所有代码使用封闭的命名空间,特别是对于带有前导下划线的名称。如果我们要省略people上面的命名空间,以引导下划线开头的全局命名空间中的名称(C++ 编译器保留的名称)可能与我们未命名命名空间中的名称不可挽回地混淆。
在我们的示例中,我们总是将关键字对象声明在(未命名的命名空间内)与使用这些关键字的启用 Boost.Parameter 的函数相同的命名空间中
namespace lib { BOOST_PARAMETER_NAME(name) BOOST_PARAMETER_NAME(index) BOOST_PARAMETER_FUNCTION( (int), f, tag, (optional (name,*,"bob")(index,(int),1)) ) { std::cout << name << ":" << index; std::cout << std::endl; return index; } }
这些函数的用户有几个选择
完全限定
int x = lib::f( lib::_name = "jill" , lib::_index = 1 );
这种方法比许多用户希望的更冗长。
通过 using-declarations 使关键字对象可用
using lib::_name; using lib::_index; int x = lib::f(_name = "jill", _index = 1);
此版本在实际调用点更好,但 using-declarations 本身可能很冗长且难以管理。
使用 using-directive 引入整个命名空间
using namespace lib; int x = f(_name = "jill", _index = 3);
此选项很方便,但它不加区别地使lib的所有内容都可用,而无需限定。
但是,如果我们在关键字声明周围添加一个额外的命名空间,我们可以给用户更多的控制权
namespace lib { namespace keywords { BOOST_PARAMETER_NAME(name) BOOST_PARAMETER_NAME(index) } BOOST_PARAMETER_FUNCTION( (int), f, keywords::tag, (optional (name,*,"bob")(index,(int),1)) ) { std::cout << name << ":" << index; std::cout << std::endl; return index; } }
现在,用户只需要一个 using-directive 即可引入与lib:
using namespace lib::keywords; int y = lib::f(_name = "bob", _index = 2);
Boost.Parameter 启用的接口习惯用法是全新的(对于 C++),因此不受现有文档约定的支持。
注意
此空间为空,因为我们尚未确定任何最佳实践。如果您有您认为值得分享的风格,我们将非常乐意链接到您的文档。
使用 Parameter 库的最新 Boost 版本的回归测试结果,了解它在您喜欢的编译器上的表现如何。此外,您可能需要注意以下问题以及特定编译器的解决方法。
如果你的编译器支持 完美转发,那么 Parameter 库将#define宏BOOST_PARAMETER_HAS_PERFECT_FORWARDING除非您手动禁用它。如果你的编译器不支持,那么parameter::parameters::operator()会将右值引用视为左值const引用,以解决 转发问题,因此在某些情况下,您必须将 boost::ref 或 std::ref 包装在任何将绑定到 out 参数的参数周围。evaluate_category.cpp 和 preprocessor_eval_category.cpp 测试程序演示了此支持。
如果你的编译器充分符合 C++11 标准,那么 Parameter 库将#define宏BOOST_PARAMETER_CAN_USE_MP11除非您手动禁用它。singular.cpp、compose.cpp、optional_deduced_sfinae.cpp 和 deduced_dependent_predicate.cpp 测试程序演示了对 Boost.MP11 的支持。
一些较旧的编译器不支持 SFINAE。如果你的编译器满足该条件,那么 Boost 标头将#define预处理器符号BOOST_NO_SFINAE,并且基于其签名,parameter-enabled 函数不会从重载集中删除。sfinae.cpp 和 optional_deduced_sfinae.cpp 测试程序演示了 SFINAE 支持。
延迟默认计算 依赖于result_of类模板来计算给定构造它们的函数对象类型的默认参数的类型。在不支持result_of, BOOST_NO_RESULT_OF的编译器上,将会被#defined,并且编译器将期望函数对象包含一个嵌套的类型名称result_type,它指示其在不带参数调用时的返回类型。要在这些编译器上使用普通函数作为默认生成器,你需要将其包装在一个提供result_type作为typedef并通过其operator().
如果你使用 Microsoft Visual C++ 6.x,你可能会发现编译器很难找到你的关键字对象。已经观察到这个问题,但仅在此编译器上观察到,并且随着测试代码的发展而消失,因此我们建议你仅将其作为最后手段,而不是作为预防措施。解决方案是添加 using-declarations 以强制名称在封闭的命名空间中可用,而无需限定
namespace graphs { using graphs::graph; using graphs::visitor; using graphs::root_vertex; using graphs::index_map; using graphs::color_map; }
点击 此链接 获取有关如何使用 Boost.Python 将启用 Boost.Parameter 的函数公开给 Python 的文档。
实际传递给函数或类模板的值。
用于在函数或类模板中引用参数的名称。例如,时,才会评估它的第二个参数。类似地,在的参数的值x由参数给出3:
int f(int x) { return x + 1; } int y = f(3);
作者要感谢所有参与审查此库及其文档的 Boosters,尤其是我们的审查经理 Doug Gregor。
[1] | 截至 Boost 1.33.0,Graph 库仍在使用旧的命名参数机制,但计划更改为使用 Boost.Parameter(此库)在即将发布的版本中,同时保持旧的界面可用以实现向后兼容。 |
[2] | 单一定义规则规定,C++ 程序中的任何给定实体都必须在组成程序的所有转换单元(对象文件)中具有相同的定义。 |
[3] | 如果你不熟悉 Boost Graph Library,请不要担心你遇到的任何 Graph 库特定细节的含义。在这种情况下,你可以用int替换文本中对顶点描述符类型的所有提及,并且你对 Parameter 库的理解不会受到影响。 |
[4] | 这是 C++20 约束背后的主要动机。 |
[5] | (1, 2) 在这里,我们假设有一个谓词元函数is_keyword_expression可以用来识别 Boost.Python 的 KeywordExpression 概念的模型。 |
[6] | 你总是可以通过应用 using-declaration 来产生函数存在于外部命名空间中的错觉 namespace foo_overloads { // foo declarations here void foo() { ... } ... } using foo_overloads::foo; 这种避免无意参数依赖查找的技术归功于 Herb Sutter。 |
[7] | 此功能取决于您的编译器对 SFINAE 的支持。SFINAE:Substitution Failure Is Not An Error(替换失败不是错误)。如果在函数模板实例化期间的类型替换导致无效类型,则不会发出编译错误;相反,重载将从重载集中删除。通过在取决于某些条件结果的函数签名中产生无效类型,我们可以决定在重载解析期间是否考虑重载。该技术在 enable_if 实用程序中进行了形式化。大多数最新的编译器都支持 SFINAE;在不支持它的编译器上,Boost 配置库将#define符号BOOST_NO_SFINAE。有关 SFINAE 的更多信息,请参见 http://www.semantics.org/once_weakly/w02_SFINAE.pdf 。 |