C++ C++;-将带有引用的长参数列表重构为结构

C++ C++;-将带有引用的长参数列表重构为结构,c++,C++,我喜欢让类在调用构造函数之后就有一个有效的状态,也就是说,所有必需的依赖项都被传递到构造函数中 我还喜欢将必需的依赖项作为引用传递,因为在编译时,nullptr只是被禁止作为这些参数的值 例如: class B; class A { public: A(B& b) : b(b) {} private: B& b; } // Includes for B, C, D, E, F... class A { public: A(B b, C c, D d

我喜欢让类在调用构造函数之后就有一个有效的状态,也就是说,所有必需的依赖项都被传递到构造函数中

我还喜欢将必需的依赖项作为引用传递,因为在编译时,nullptr只是被禁止作为这些参数的值

例如:

class B;

class A
{
public:
    A(B& b) : b(b) {}

private:
    B& b;
}
// Includes for B, C, D, E, F...

class A
{
public:
    A(B b, C c, D d, E e, F f) : b(b), c(c), d(d), e(e), f(f) {}

private:
    B b;
    C c;
    D d;
    E e;
    F f;
}
在实例化之后,可以(几乎)保证实例处于有效状态。我发现这种代码风格非常安全,不会出现编程错误

我的问题是当这些类有很多依赖项时重构它们

例如:

class B;

class A
{
public:
    A(B& b) : b(b) {}

private:
    B& b;
}
// Includes for B, C, D, E, F...

class A
{
public:
    A(B b, C c, D d, E e, F f) : b(b), c(c), d(d), e(e), f(f) {}

private:
    B b;
    C c;
    D d;
    E e;
    F f;
}
通常,我会在结构中放置一长串参数,如下所示:

struct Deps
{
    B b;
    C c;
    D d;
    E e;
    F f;
}

class A
{
public:
    A(Deps deps) : b(deps.b), c(deps.c), d(deps.d), e(deps.e), f(deps.f) {}

private:
    B b;
    C c;
    D d;
    E e;
    F f;
}
这样,它使调用站点更显式,也更不容易出错:因为所有参数都必须命名,所以您不会因为顺序错误而错误地切换其中两个参数

遗憾的是,这种技术在引用时效果不佳。在Deps结构中有引用会将问题转发给该结构:然后,Deps结构需要有一个初始化引用的构造函数,然后该构造函数将有一个很长的参数列表,实际上什么也解决不了


现在来问这个问题:有没有办法在包含引用的构造函数中重构长参数列表,这样就没有函数会产生长参数列表,所有参数都是有效的,并且类的任何实例都不会处于无效状态(例如,某些依赖项未初始化或为null)?

如果您对运行时完整性检查没有问题,我建议将指针存储在
Deps
中,并在
a
的构造函数中检查所有指针是否为非空。这允许您以增量方式构建
Deps
,并且与以前一样安全。要在取消引用指针之前执行非空检查,您可能需要一些麻烦(比如逗号运算符)。您还可以存储指针,而不是
A
成员的引用,因为(如果构造函数检查空值),它同样安全,但允许赋值运算符。并使空检查更容易:

struct Deps
{
    B* b;
    C* c;
    D* d;
    E* e;
    F* f;
};

template<class ... Ts>
bool allNonNull(Ts* ... ts)
{
    return ((ts != nullptr) && ...);
}

class A
{
public:
    A(Deps deps) : b(deps.b), c(deps.c), d(deps.d), e(deps.e), f(deps.f)
    {
        assert(allNonNull(b, c, d, e, f));
        if (!allNonNull(b, c, d, e, f))
            /*whatever error handling you want*/;
    }
private:
    B* b;
    C* c;
    D* d;
    E* e;
    F* f;
};
struct Deps
{
B*B;
C*C;
D*D;
E*E;
F*F;
};
模板
bool allNonNull(Ts*…Ts)
{
返回((ts!=nullptr)和&…);
}
甲级
{
公众:
A(副部长):b(副部长b)、c(副部长c)、d(副部长d)、e(副部长e)、f(副部长f)
{
断言(allNonNull(b,c,d,e,f));
如果(!allNonNull(b,c,d,e,f))
/*无论您想要什么样的错误处理*/;
}
私人:
B*B;
C*C;
D*D;
E*E;
F*F;
};

缺点当然是没有更多的编译时检查,并且存在大量代码重复。还可能忘记更新null check函数的参数。

你不能既吃蛋糕又吃蛋糕。除非你使用魔法(也称为更强大的类型)

让构造函数获取所有必要的依赖项的关键思想是确保它们都是在构造发生时提供的,并静态地强制执行。如果将此负担移动到结构,则仅当所有字段都已填充时,此结构才应传递给构造函数。如果您有未包装的引用,则显然不可能仅部分填充此结构,并且您无法向编译器证明稍后将提供所需的参数

当然,您可以进行运行时检查,但这不是我们想要的。理想情况下,我们能够对类型本身中已初始化的参数进行编码。这很难以通用的方式实现,如果您做出一些让步并为特定类型手工编写,那么实现起来就稍微容易一些

考虑一个简化的示例,其中类型在签名中不重复(例如,构造函数的签名是
ctor(int,bool,string)
)。然后,我们可以使用
std::tuple
表示部分填充的参数列表,如下所示:

auto start = tuple<>();
auto withIntArg = push(42, start);
auto withStringArg = push("xyz"s, withIntArg);
auto withBoolArg = push(true, withStringArg);
auto start=tuple();
auto withIntArg=按下(42,启动);
自动带串=推动(“xyz”s,带插入标记);
auto with Boolarg=推送(true,with String);
我已经使用了
auto
,但是如果您考虑这些变量的类型,您会意识到只有在所有这些变量都被执行之后(尽管是以随机顺序),它才会到达所需的
元组。然后,您可以将类构造函数作为模板编写,只接受确实具有所有必需类型的元组,编写
push
函数,瞧

当然,这是大量的样板文件,并且可能会出现非常严重的错误,除非您在编写上述内容时非常小心。你想做的任何其他解决方案都需要有效地做同样的事情;修改部分填充的参数列表的类型,直到它适合所需的集


值得吗?好吧,你自己决定。

实际上,有一个非常优雅/简单的解决方案,使用
std::tuple

#include <tuple>

struct A{};
struct B{};
struct C{};
struct D{};
struct E{};
struct F{};

class Bar
{
public:
    template<class TTuple>
    Bar(TTuple refs)
        : a(std::get<A&>(refs))
        , b(std::get<B&>(refs))
        , c(std::get<C&>(refs))
        , d(std::get<D&>(refs))
        , e(std::get<E&>(refs))
        , f(std::get<F&>(refs))
    {
    }

private:
    A& a;
    B& b;
    C& c;
    D& d;
    E& e;
    F& f;
};


void test()
{
    A a; B b; C c; D d; E e; F f;
    // Different ways to incrementally build the reference holder:
    auto tac = std::tie(a, c); // This is a std::tuple<A&, C&>.
    auto tabc = std::tuple_cat(tac, std::tie(b));
    auto tabcdef = std::tuple_cat(tabc, std::tie(d, f), std::tie(e));

    // We have everything, let's build the object:
    Bar bar(tabcdef);
}
#包括
结构A{};
结构B{};
结构C{};
结构D{};
结构E{};
结构F{};
分类栏
{
公众:
模板
条形图(三倍参考)
:a(std::get

std::tie
的存在正是为了创建一个引用元组。我们可以使用
std::tuple\u cat
组合引用元组。
std::get
允许从给定元组中准确检索所需的引用

这包括:

  • 最小样板:对于每个引用类型,您只需在成员初始值设定项列表中写入
    std::get
    。对于更多引用或包含引用的类型,不需要提供/重复任何其他内容

  • 完整的编译时安全性:如果忘记提供引用或提供引用两次,编译器会抱怨。类型系统编码所有必要的信息

  • 对添加引用的顺序没有限制