【问题标题】:Writing testable code - a basic_istream factory within lambda functions and unique_ptr编写可测试代码 - lambda 函数和 unique_ptr 中的 basic_istream 工厂
【发布时间】:2020-03-13 08:38:36
【问题描述】:

简介

这是一个评估我的方法的请求,在我还没有信心的现代 C++ 功能的上下文中,比如 lambda 函数和basic_istream。最后我有一个简短的具体问题列表,所有这些问题都与同一个问题有关。

背景

我正在使用 Boost::program_options (1.69) 用 C++14 编写配置文件解析器,特别是 boost::program_options::parse_config_file() 函数。这个函数(在boost/program_options/parsers.hpp 中定义)有两个覆盖。

第一个从磁盘上的文件中读取配置:

basic_parsed_options<charT>
parse_config_file(const char* filename, const options_description&,
                  bool allow_unregistered = false);

第二个从std::basic_istream&lt;charT&gt; 引用中读取配置:

basic_parsed_options<charT>
parse_config_file(std::basic_istream<charT>&, const options_description&,
                  bool allow_unregistered = false);

我正在编写单元测试(使用 Google 测试),以便我可以将配置文件的内容传递给函数,而不是像在生产中那样从实际文件中读取测试。这是为了避免必须创建临时配置文件,避免并行运行测试时发生冲突等。也许这种方法可能会更好,但是能够从字符串而不是文件提供配置似乎并没有错。

我还要补充一点,我不能简单地将 istream 传递给 process_config 函数,因为在完整的实现中,文件名实际上是在函数本身内确定的。 IE。首先解析命令行,并从中获取配置文件名。因此在函数执行之前文件名是未知的。

实施

被测函数原来是这样的:

struct AppConfig {
    bool myoption1 {false};
};

void process_config(int argc, char * argv[], AppConfig & config) {
    // ...
    po::parse_config_file("config.cfg", config_file_options);
    // ...
}

问题是这个接口没有提供注入配置文件内容的方法,所以我考虑传入一个process_config() 可以调用的回调,以便返回一个可以传递给@987654332 的basic_istream&lt;charT&gt; @。这样,测试可以传入一个回调,该回调仅提供一个 std::istringstreambasic_istream 模板的子类)并预先加载了配置文件内容,生产客户端可以传入一个简单的回调,该回调只需打开指定磁盘上的文件,并返回相应的 istream。

为此,我创建了这个:

template <typename funcF>
void process_config(int argc, char * argv[], AppConfig & config, funcF get_istream) {

    namespace po = boost::program_options;

    po::options_description config_file_options;
    config_file_options.add_options() /* ... */;

    auto istr_up = get_istream("config.cfg");
    auto & istr = *istr_up.get();
    po::parse_config_file(istr, config_file_options);
}

然后在生产代码中,我可以使用它从配置文件中返回 ifstream:

auto make_config_istream(const std::string & s) {
    return std::unique_ptr<std::ifstream>(s.c_str());
}

在测试代码中,我可以使用它从测试中定义的字符串返回配置文件内容:

std::unique_ptr<std::basic_istream<char>> make_istream(const std::string & s) {
    return std::make_unique<std::istringstream>(s);
}

process_config(0, nullptr, config,
               [](const std::string &){ return make_istream("foo=1\nbar=2");} );

这似乎按我的预期编译和执行。但是它看起来有点笨拙,因为 lambda 签名必须存在被忽略的 const std::string &amp; 才能匹配 process_config 模板,所以我尝试添加这个额外的辅助函数来隐藏它:

auto make_config(const std::string & config) {
    // the std::string parameter is ignored:
    return [config](const std::string &) { return make_istream(config); };
}

然后将测试代码改为:

process_config(0, nullptr, config, make_config("foo=1\nbar=2"));

这个语法可以满足我的要求。

请注意,我将在唯一指针中传回 istream 对象。我不是 100% 确定这是正确的方法,但是因为 istream 的默认构造函数被禁用,我不能通过值传递它们,所以似乎我必须在堆上分配它们并传递它们用指针环绕。

问题

感谢您阅读本文。我的问题是:

  1. 通过使用unique_ptr 在堆上返回新建的istream 是否合适/最佳实践?有没有更好的方法来做到这一点?
  2. 使用auto &amp; istr = *istr_up.get() 取消引用unique_ptr 以访问对istream 的引用的方法是否正确?这对我来说很难闻。
  3. 使用模板化的process_config 函数是处理get_istream 参数中涉及的类型的最佳方式吗? auto 有没有更好的方法?
  4. 最后,有没有更好的方法来解决整个问题?

【问题讨论】:

  • I/O 流自 C++11 以来是可移动的,因此在这种情况下不需要堆分配。
  • @0x499602D2 我认为这可能是真的,但是当我尝试std::move 返回值时,编译器抱怨我返回了对本地分配对象的引用。如果我不使用 std::move 并尝试按值传递,则会收到默认复制构造函数已禁用的错误。
  • 如果不需要模板,可以使用擦除类型std::function&lt;std::istream(const std::string&amp;)&gt;或自定义界面。

标签: c++ unit-testing lambda c++14 boost-program-options


【解决方案1】:

通过使用 unique_ptr 在堆上返回新构建的 istream 是否合适/最佳实践?有没有更好的方法来做到这一点?

basic_istream是可移动的,你不需要unique_ptr

auto make_config_istream(const std::string & s) {
    return std::ifstream(s);
}

auto make_istream(const std::string & s) {
    return std::istringstream(s);
}

template <typename funcF>
void process_config(int argc, char * argv[], AppConfig & config, funcF get_istream) {

    namespace po = boost::program_options;

    po::options_description config_file_options;
    config_file_options.add_options() /* ... */;

    auto istr = get_istream("config.cfg");
    po::parse_config_file(istr, config_file_options);
}

其余的可以保持原样。

使用 auto & istr = *istr_up.get() 取消引用 unique_ptr 以访问对 istream 的引用的方法是否正确?这对我来说很难闻。

这是正确的,但不必要的复杂,因为auto &amp; istr = *istr_up; 也是如此。您也可以像使用原始指针一样使用istr_up(即使用-&gt;*)。

使用模板化的 process_config 函数是处理 get_istream 参数中涉及的类型的最佳方式吗?有没有更好的方法来使用 auto 呢?

是的,这是正确的做法,但需要将 parse_config 移动到所有使用它的翻译单元共享的标头中,否则需要格外小心以在必要时正确显式实例化。


process_config 的用法对我来说似乎太复杂了,为什么不呢:

process_config(0, nullptr, config,
    [](auto){return std::istringstream("foo=1\nbar=2");})

process_config(0, nullptr, config,
    [](auto s){return std::ifstream(s);})

【讨论】:

  • 感谢您的回答。我很感激反馈。我不介意您对 process_config 用法的建议更改,我只是想稍微减少它的冗长性,因为它会在整个测试套件中重复多次。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-12
  • 1970-01-01
  • 1970-01-01
  • 2010-12-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多