这个问题有几种解决方案。虽然最初看起来这应该是一项简单的任务,但它通常相当复杂。这是因为节大致相当于命名空间;部分不等同于对象。
[slave]
address=localhost
port=1111
[slave]
address=192.168.0.1
port=2222
上面的配置有一个slave
命名空间,包含两个address
值和两个port
价值观。没有两个slave
每个对象都有一个address
and port
。由于这种区别,关联值或配对必须在应用程序代码中完成。这提供了以下选项:
- 使用配置文件的布局来暗示配对。
- 通过将多个值合并为单个字段值来执行显式配对。
隐含配对
通过这种方法,配置文件可以保持原样。这种方法的简单性取决于:
- 一些 Boost.ProgramOption 组件的行为。
- 每个对象表示为一个没有可选字段和少量字段的部分。
[slave]
address=localhost # slave.address[0]
port=1111 # slave.port[0]
[slave]
address=192.168.0.1 # slave.address[1]
port=2222 # slave.port[1]
不修改配置,代码如下:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/program_options.hpp>
/// @brief Convenience function for when a 'store_to' value is being provided
/// to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
std::string address;
unsigned short port;
/// @brief Constructor.
slave( std::string address,
unsigned short port )
: address( address ),
port( port )
{}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream,
const slave& slave )
{
return stream << "Slave address: " << slave.address
<< ", port: " << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const std::string& address,
unsigned short port )
{
return slave( address, port );
}
int main()
{
// Variables that will store parsed values.
std::vector< std::string > addresses;
std::vector< unsigned short > ports;
// Setup options.
namespace po = boost::program_options;
po::options_description desc( "Options" );
desc.add_options()
( "slave.address", make_value( &addresses ),
"slave's hostname or ip address" )
( "slave.port" , make_value( &ports ),
"plugin id" );
// Load setting file.
po::variables_map vm;
std::ifstream settings_file( "config.ini", std::ifstream::in );
po::store( po::parse_config_file( settings_file , desc ), vm );
settings_file.close();
po::notify( vm );
// Transform each address and port pair into a slave via make_slave,
// inserting each object into the slaves vector.
std::vector< slave > slaves;
std::transform( addresses.begin(), addresses.end(),
ports.begin(),
std::back_inserter( slaves ),
make_slave );
// Print slaves.
std::copy( slaves.begin(), slaves.end(),
std::ostream_iterator< slave >( std::cout, "\n" ) );
}
产生以下输出:
Slave address: localhost, port: 1111
Slave address: 192.168.0.1, port: 2222
基本显式配对
有时可以在单个字段中以有意义的方式表示多个值。两者的一种共同表示address
and port
is address:port
。通过这种配对,生成的配置文件将如下所示:
[slaves]
slave=localhost:1111
slave=192.168.0.1:2222
这种方法的简单性取决于:
- 能够将多个值表示为单个有意义的值,而无需键说明符。
- 每个对象没有可选值。
更新后的代码:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>
/// @brief Convenience function for when a 'store_to' value is being provided
/// to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
std::string address;
unsigned short port;
/// @brief Constructor.
slave( std::string address,
unsigned short port )
: address( address ),
port( port )
{}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream,
const slave& slave )
{
return stream << "Slave address: " << slave.address
<< ", port: " << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const std::string& address_and_port )
{
// Tokenize the string on the ":" delimiter.
std::vector< std::string > tokens;
boost::split( tokens, address_and_port, boost::is_any_of( ":" ) );
// If the split did not result in exactly 2 tokens, then the value
// is formatted wrong.
if ( 2 != tokens.size() )
{
using boost::program_options::validation_error;
throw validation_error( validation_error::invalid_option_value,
"slaves.slave",
address_and_port );
}
// Create a slave from the token values.
return slave( tokens[0],
boost::lexical_cast< unsigned short >( tokens[1] ) );
}
int main()
{
// Variables that will store parsed values.
std::vector< std::string > slave_configs;
// Setup options.
namespace po = boost::program_options;
po::options_description desc( "Options" );
desc.add_options()
( "slaves.slave", make_value( &slave_configs ),
"slave's address@port" );
// Load setting file.
po::variables_map vm;
std::ifstream settings_file( "config.ini", std::ifstream::in );
po::store( po::parse_config_file( settings_file , desc ), vm );
settings_file.close();
po::notify( vm );
// Transform each config into a slave via make_slave, inserting each
// object into the slaves vector.
std::vector< slave > slaves;
std::transform( slave_configs.begin(), slave_configs.end(),
std::back_inserter( slaves ),
make_slave );
// Print slaves.
std::copy( slaves.begin(), slaves.end(),
std::ostream_iterator< slave >( std::cout, "\n" ) );
}
产生相同的输出:
Slave address: localhost, port: 1111
Slave address: 192.168.0.1, port: 2222
值得注意的代码修改如下:
- The
options_description
's options
需要读书slaves.slave
as a std::vector< std::string >
.
-
make_slave
将采取单一std::string
参数,从中提取address
and port
.
- 更新
std::transform
调用仅迭代一个范围。
高级显式配对
通常,多个字段无法有意义地表示为单个无键值,或者对象具有可选字段。对于这些情况,需要进行额外级别的语法和解析。虽然应用程序可以引入自己的语法和解析器,但我建议利用 Boost.ProgramOption 的命令行语法(--key value
and --key=value
)和解析器。生成的配置文件可能如下所示:
[slaves]
slave= --address localhost --port 1111
slave= --address = 192.168.0.1 --port=2222
更新后的代码:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/bind.hpp>
#include <boost/program_options.hpp>
#include <boost/tokenizer.hpp>
// copy_if was accidently left out of the C++03 standard, so mimic the
// C++11 behavior to support all predicate types. The alternative is to
// use remove_copy_if, but it only works for adaptable functors.
template < typename InputIterator,
typename OutputIterator,
typename Predicate >
OutputIterator
copy_if( InputIterator first,
InputIterator last,
OutputIterator result,
Predicate pred )
{
while( first != last )
{
if( pred( *first ) )
*result++ = *first;
++first;
}
return result;
}
/// @brief Tokenize a string. The tokens will be separated by each non-quoted
/// character in @c separator_characters. Empty tokens are removed.
///
/// @param input The string to tokenize.
/// @param separator_characters The characters on which to delimit.
///
/// @return Vector of tokens.
std::vector< std::string > tokenize( const std::string& input,
const std::string& separator_characters )
{
typedef boost::escaped_list_separator< char > separator_type;
separator_type separator( "\\", // The escape characters.
separator_characters,
"\"\'" ); // The quote characters.
// Tokenize the intput.
boost::tokenizer< separator_type > tokens( input, separator );
// Copy non-empty tokens from the tokenizer into the result.
std::vector< std::string > result;
copy_if( tokens.begin(), tokens.end(), std::back_inserter( result ),
!boost::bind( &std::string::empty, _1 ) );
return result;
}
/// @brief option_builder provides a unary operator that can be used within
/// stl::algorithms.
template < typename ResultType,
typename Builder >
class option_builder
{
public:
typedef ResultType result_type;
public:
/// @brief Constructor
option_builder( const boost::program_options::options_description& options,
Builder builder )
: options_( options ),
builder_( builder )
{}
/// @brief Unary operator that will parse @c value, then delegate the
/// construction of @c result_type to the builder.
template < typename T >
result_type operator()( const T& value )
{
// Tokenize the value so that the command line parser can be used.
std::vector< std::string > tokens = tokenize( value, "= " );
// Parse the tokens.
namespace po = boost::program_options;
po::variables_map vm;
po::store( po::command_line_parser( tokens ).options( options_ ).run(),
vm );
po::notify( vm );
// Delegate object construction to the builder.
return builder_( vm );
}
private:
const boost::program_options::options_description& options_;
Builder builder_;
};
/// @brief Convenience function used to create option_builder types.
template < typename T,
typename Builder >
option_builder< T, Builder > make_option_builder(
const boost::program_options::options_description& options,
Builder builder )
{
return option_builder< T, Builder >( options, builder );
}
/// @brief Convenience function for when a 'store_to' value is being provided
/// to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
std::string address;
unsigned short port;
/// @brief Constructor.
slave( std::string address,
unsigned short port )
: address( address ),
port( port )
{}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream,
const slave& slave )
{
return stream << "Slave address: " << slave.address
<< ", port: " << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const boost::program_options::variables_map& vm )
{
// Create a slave from the variable map.
return slave( vm["address"].as< std::string >(),
vm["port"].as< unsigned short >() );
}
int main()
{
// Variables that will store parsed values.
std::vector< std::string > slave_configs;
// Setup options.
namespace po = boost::program_options;
po::options_description desc( "Options" );
desc.add_options()
( "slaves.slave", make_value( &slave_configs ),
"slave's --address ip/hostname --port num" );
// Load setting file.
po::variables_map vm;
std::ifstream settings_file( "config.ini", std::ifstream::in );
po::store( po::parse_config_file( settings_file , desc ), vm );
settings_file.close();
po::notify( vm );
// Create options for slaves.slave.
po::options_description slave_desc( "Slave Options" );
slave_desc.add_options()
( "address", po::value< std::string >(),
"slave's hostname or ip address" )
( "port" , po::value< unsigned short >(),
"slave's port" );
// Transform each config into a slave via creating an option_builder that
// will use the slave_desc and make_slave to create slave objects. These
// objects will be inserted into the slaves vector.
std::vector< slave > slaves;
std::transform( slave_configs.begin(), slave_configs.end(),
std::back_inserter( slaves ),
make_option_builder< slave >( slave_desc, make_slave ) );
// Print slaves.
std::copy( slaves.begin(), slaves.end(),
std::ostream_iterator< slave >( std::cout, "\n" ) );
}
产生与之前的方法相同的输出:
Slave address: localhost, port: 1111
Slave address: 192.168.0.1, port: 2222
值得注意的代码修改如下:
- Created
copy_if
因为它是 C++03 中一个被忽视的算法。
- 使用 Boost.Tokenizer 而不是 Boost.StringAlgo,因为 Boost.Tokenizer 可以更轻松地处理引用转义。
- 创建了一个
option_builder
一元函子有助于为应用转换提供惯用的重用。
-
make_slave
现在需要一个boost::program_options::variables_map
它将从中构建一个slave
object.
这种方法还可以轻松扩展以支持以下变体:
-
支持单个值的多个命令行。例如,一个配置可以支持两个从属设备,其中一个从属设备具有辅助配置,以防第一个从属设备发生故障。这需要对,
分隔符。
[slaves]
slave = --address localhost --port 1111, --address 127.0.0.1 --port 1112
slave = --address 192.168.0.1 --port 2222
声明选项slave_desc
as typed_value
与提供给变量store_to
争论。然后可以将这些相同的变量绑定boost::ref
via boost::bind
to the make_slave
工厂功能。虽然这解耦了make_slave
从 Boost.ProgramOptions 类型来看,对于具有许多字段的类型来说,维护可能会变得困难。
替代方法
替代方法仍然需要通过将多个值放入单个值来完成显式配对。但是,在解析阶段可以通过继承任一对象来进行转换boost::program_options::typed_value
or boost::program_options::untyped_value
.
- 当继承自
typed_value
,覆盖parse
功能。使用的后果之一typed_value
是模板参数必须满足所有要求typed_value
。例如,如果typed_value< slave >
被使用,那么它需要制作slave
默认可构造,并定义两者istream
提取(>>
) and ostream
插入(<<
) 运算符slave
.
- 当继承自
untyped_value
,覆盖两者parse
and notify
功能。这种方法不会强加类型要求,例如typed_value
,但它确实要求派生类维护自己的store_to
多变的。
建议
- 当绝对确定永远不会有可选字段并且字段数量将最少(2~)时,则使用隐含配对方法。
- 如果存在最少量的字段 (2~),并且可以在没有字段名称标识符的情况下以有意义的方式表示值,则使用基本显式配对。可以支持可选字段,但它增加了语法和解析器的复杂性。
- 对于所有其他情况或存在任何不确定性时,请使用高级显式配对。虽然这可能需要更多的工作,但它提供了更好的可重用性。例如,如果从属配置变得非常复杂,以至于每个从属都有自己的配置文件,那么代码更改很小,因为只需要更改解析器类型和调用。