【问题标题】:Make argument mandatory without relying on position在不依赖立场的情况下强制论证
【发布时间】:2014-11-11 19:13:57
【问题描述】:

假设一个类 Foo 有两个依赖项(Bar 和 Baz),并且在不提供这两个依赖项的情况下构造一个 Foo 是错误的。构造函数注入可以很容易地在编译时保证这样做:

class Foo
{
public:
   Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz);
   // (don't get hung up on the type of pointer used; it's for example only)
};

但是假设 Foo 也需要两个双打:

class Foo
{
public:
    Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz,
        double val1, double val2);
};

现在有问题了;调用者很容易意外地转置 val1 和 val2 并创建运行时错误。我们可以添加一个 Params 结构来允许命名初始化并排除这种情况:

class Foo
{
public:
   struct Params
   {
       std::shared_ptr<Bar> bar;
       std::shared_ptr<Baz> baz;
       double val1;
       double val2
   };

    Foo(const Params& params);
};

// ...

std::shared_ptr<Foo> MakeDefaultFoo()
{
    Foo::Params p;
    p.bar = std::make_shared<Bar>();
    p.baz = std::make_shared<Baz>();
    p.val1 = 4.0;
    p.val2 = 3.0;
    return std::make_shared<Foo>(p);
}

但现在我们遇到的问题是调用者可能会忘记填充 Params 中的某个字段,这要到运行时才能检测到。 struct 初始化语法或初始化列表将不可能忘记一个字段,但是我们又回到了依赖位置!

是否有一些技巧可以使两全其美——编译器强制强制参数按名称而不是位置分配?

【问题讨论】:

  • 您可以添加Params() : val1(0), val2(0) { },这样如果未设置值,它至少具有一些合理的默认值。
  • @cdhowie 当我不得不求助于它时,我实际上更喜欢 insane 默认值,这样如果发生这种情况,代码将很快失败。但最好在编译时失败!
  • 或者说真的,也许不是double 有两个不同命名类型的两个变量?
  • @dlf 在运行时失败是否可以接受,但在调用Foo ctor 时立即失败?
  • @cdhowie 总比没有好(这就是我现在所做的),但我宁愿在编译时失败。如果将来不更改语言,我真的不相信有解决这个问题的方法,但我猜你永远不知道。

标签: c++ initialization constructor-injection


【解决方案1】:

只要有一个简单的包装器就可以工作:

template <typename Tag, typename T>
struct Argument {
    explicit Argument( const T &val );
    T get() const;
};

class Foo {
public:
       struct Val1Tag;
       struct Val2Tag;
       typedef Argument<Val1Tag,double> Val1;
       typedef Argument<Val2Tag,double> Val2;

       Foo( Val1 v1, Val2 v2 );

};

Foo foo( Foo::Val1( 1.0 ), Foo::Val2( 2.3 ) );

现在类型是显式的,如果没有编译器错误,您无法交换它们。

【讨论】:

  • -1 只是Val1Val2指的是同一个类型,所以可以互换。
  • Here is a demo of what I mean。这种方法根本无法完成任何事情。
  • 不错的修复和巧妙的技术。请注意,您确实需要在Foo 之外完全限定Val1。 (Foo foo(Foo::Val1(...)
  • 有趣的问题,标签结构必须完整吗?快速搜索此处不显示答案...
  • 不是在这种情况下,不。事实上,您甚至不必命名标签模板参数(template &lt;typename, typename T&gt; 就可以了)。类型模板参数本身并不要求类型是完整的,而是取决于您对该模板参数做了什么——在这种情况下,您什么也不做。
【解决方案2】:

非常想知道 cdhowie 正在修补什么,但与此同时,具有不同类型的简单包装器可能会解决一些问题:

struct Val1 {
    explicit Val1(double v) : v(v) { }
    operator double() const { return v; }

    double v;
};

// copy for Val2

class Foo
{
public:
    Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz,
        Val1 val1, Val2 val2);
};

这样你就不能把它们混在一起,因为你必须像这样构造一个 Foo:

Foo foo(bar, baz, Val1{3.0}, Val2{7.0});

为了确保类型不同,需要进行大量额外输入,并且您必须确保创建构造函数explicit(否则它会失败),但这会有所帮助。

【讨论】:

  • 不是最糟糕的主意。请注意,如果您实现了一个基础template &lt;typename T&gt; class Wrapper ...,那么您可以简单地继承它并将基础构造函数与using 一起引入。那么Val1Val2 几乎可以是一条直线。
【解决方案3】:

类似这样的东西(未经测试)

template <typename tag, typename t>
struct param
{
   explicit param(t vv)
    : v(vv) {}
   param(const param& p)
    : v(p.v) {}
   t v; 
};

struct one{}; struct two {};
using paramone = param<one, double>;
using paramtwo = param<two, double>;

void somefunc (paramone p1, paramtwo p2)
{ ... };
void somefunc (paramtwo p2, paramone p1) 
{ somefunc(p1, p2); }

// using it

somefunc (2, 3); // bad
somefunc (paramone(2), paramtwo(3)); // good
somefunc (paramtwo(3), paramone(2)); // also good

【讨论】:

    猜你喜欢
    • 2015-06-08
    • 2010-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多