可变数量的(const)引用参数

时间:2014-05-12 15:43:33

标签: c++ c++11

我试图从我的高级代码中消除原始指针(使用C ++ 11),我发现在很多情况下(尤其是const)的引用是很好的替代品没有所有权转移)。

但是,如果有变量我希望通过(const)引用传递的参数数量该怎么办?

您无法创建std::vectorstd::initializer_list个引用,因为这些容器在内部使用存储元素的地址,但引用没有地址。

可能会使用std::vector<std::reference_wrapper<T>>,但这需要笨拙的客户端代码,例如doSomething({std::ref(A()), std::ref(B()), std::ref(C())}),而不是doSomething({A(), B(), C()})AB }和C是从T派生的类。

是否可以使用其他容器?或者可能使用可变参数模板?

4 个答案:

答案 0 :(得分:2)

使用可变参数模板函数执行此操作。当然不需要将T容器传递给函数,以替代T的变量参数列表。

这是一个示例C ++ 11程序:

#include <iostream>
#include <vector>

// Base case with 0 arguments
std::vector<int> foo() {
    return std::vector<int>();
}

// General case with 1 + N arguments of type `int`.
// Return a `vector<int>` populated with the arguments. 
template<typename ...Args>
std::vector<int> foo(int const & first, Args const &... rest)
{
    std::vector<int> h(1,first);
    if (sizeof...(rest)) {
        std::vector<int> t = foo(rest...);
        h.insert(h.end(),t.begin(),t.end());
    }
    return h;

}

struct bar{};

using namespace std;

int main()
{
    int i = 1, j = 2, k = 3;

    vector<int> v0 = foo(i,j);
    vector<int> v1 = foo(i,j,k);
    cout << v0.size() << endl;
    cout << v1.size() << endl;
    // bar b;
    // vector<int> v2 = foo(i,j,k,b); <-- Compile error  
    return 0;
}

乍一看,似乎是foo在一般情况下的定义 case不会将所有参数约束为(可转换为)int, 但实际上他们必须 - 见证无法编译的初始化 v2

继续回应OP的评论

如何编写一个类型安全函数,可以采用任意给定的多个参数 类型,包括const引用类型,在C ++ 11中不是一个有问题的问题。 核心语言提供了这种语法模式:

// Base case, 0 arguments
R Func() { [---] }

// General case, 1 + N arguments
template<typename U ...Args>
R Func(T [Modifier], Args [Modifier] ...args) {
    [---]
    if (sizeof...(args)) {
        [---Func(args)---]
    }
    [---]
}

可填写[---][---Func(args)---]

上面示例程序中的函数模板foo应用此模式。 您问:如果 foo 做的事情比创建容器更复杂怎么办? 答案是:无论出现什么并发症,都要适当地应用模式 是 - 就像你应用模式一样:

for( [---];[---];[---]) {
    [---]
}

适当的,无论是什么并发症。可变函数模板 模式需要更多的习惯因为它涉及递归模板 实例化 - 这就是全部。

你似乎混淆了两件事:

  • A)接受类型为T的可变数量参数的函数。
  • B)接受C类型的一个参数的函数,其中C是 类型为T的对象的可迭代序列。

在你自己的回答中你说:

  

采用可变数量参数的函数可写为:

void foo(std::initializer_list<rvalue_reference_wrapper<Base>> args)
{
    for (Base& arg : args)
    {
        arg.virtFunc();
        doStuffWithBaseRef(arg);
    }
}

那只是 A),它是B)。

在这里和你的评论中,你表现出希望能够迭代 函数体内可变函数的参数。在C / C ++中,有 没有迭代函数参数的机制,(除非它是 每个标准C varargs function 而你还没有发明一个。如果函数是B)类型,那么显然是 函数可以遍历作为成员的T C作为函数的参数。 这就是你在答案中foo所做的事情。

如果在C ++中不可能编写A类函数,那么就作为kludge 我们可以替换B)类型的函数。但是A)类型的功能是 使用所示的类型安全的可变参数模板模式进行常规编码,并且没有这样的kludge 被要求。如果您想要的是A)类型的函数,请使用该模式 并掌握它。如果你想要的是一个迭代的函数 T序列的成员,然后像你所做的那样做:写一个需要的 参数是T的可迭代序列。

被认为是传递[const]的可迭代序列的可能方法 引用到函数,您的解决方案具有禁用限制 这些引用只能引用所在的临时对象 在初始化列表中构造,而不是对预先存在的对象的引用 - 因为他们几乎总是在真实的代码中。所以,例如,虽然 代码:

foo({Derived1(), Derived2()});

将根据预期编译并运行fooDerived1的定义, 在你的回答中Derived2,更有可能的情况是:

Derived1 d1; // <- Comes from somewhere
Derived2 d2; // <- Comes from somewhere
foo({d1,d2}); // <- Error

将无法编译,因为左值T无法绑定到T&&。到处走走 这个,你必须写:

Derived1 d1; // <- Comes from somewhere
Derived2 d2; // <- Comes from somewhere
foo({Derived1(d1),Derived2(d2)});

现在,您正在构建&#34;参数&#34;的临时副本,并且 initalizer_list rvalue_reference_wrapperfoo的引用 临时副本,以便您可以在foo内迭代对临时对象的引用。

好吧,如果你必须使用&#34;参数&#34;的副本,这是多余的 打扰构建一系列对副本的引用。只是 复制&#34;参数&#34;进入任何合适的容器并传递foo一个[const]引用 那个。这不会阻止B迭代[const]引用 容器成员就像现在一样。

你似乎可能部分地通过这个问题来行使:什么 对于衍生自多态的各种类型的物体,它将是一个合适的容器 base std:: ,如果不是动态分配对象的原始指针容器?

无可争议的答案是:<std::shared_ptr<B>> 容器 std::shared_ptr<T>, 其中 Container 是标准容器模板(vector,list等) 为您的应用程序提供适当的界面。更普遍的是所谓的 智能指针模板,std::unique_ptr<T>documentation)和 std::initializer_listdocumentation) 是用于避免暴露原始动态指针的标准C ++ 11资源。

您似乎也被void foo(std::initializer_list<std::shared_ptr<Base>> args) { for (auto arg : args) { arg->virtFunc(); doStuffWithBaseRef(*arg); } } std::shared_ptr<Base> b1(new Derived1); std::shared_ptr<Base> b2(new Derived2); foo({b1,b2}); 吸引了 将可迭代序列传递给函数,因为您很容易 可以使用支撑初始化器在使用点构造一个。那方便 可以在不处理原始动态指针智能指针的情况下保留。 E.g。

void foo(std::initializer_list<Base *> args)
{
    for (auto arg : args)
    {
        arg->virtFunc();
        doStuffWithBaseRef(*arg);
    }
}

Derived1 d1;
Derived2 d2;
foo({&d1,&d2});

会很好,所以会:

{{1}}

答案 1 :(得分:0)

我找到了一种有效的方法,但它可能不是理想的解决方案。

std::reference_wrapper的问题在于它无法通过右值引用创建。所以我写了这样的any_reference_wrapper

template<class T>
class any_reference_wrapper
{
public:
    any_reference_wrapper(T&& t) : ref(t) {}
    any_reference_wrapper(T& t) : ref(t) {}

    operator T&() const {return ref;}
    T& get() const {return ref;}
private:
    T& ref;
};

然后我们可以通过介绍来简化我们的生活

template<class T> using any_reference_initializer_list = 
    std::initializer_list<any_reference_wrapper<T>>;

语义上接受可变数量参数的函数(但在技术上需要一个容器)可以写成:

void foo(any_reference_initializer_list<Base> args)
{
    for (Base& arg : args)
    {
        arg.virtFunc();
        doStuffWithBaseRef(arg);
    }
}

可以用以下的临时工具调用:

foo({Derived1(), Derived2()});

但也引用了现有的变量:

Derived d1;
foo({d1, Derived2()});

优点:

  • foo不一定是模板函数(如果可能,我喜欢避免使用模板)。
  • 一个简单的for循环可以在foo中使用,没有奇怪的递归会让读者感到困惑。
  • 来电者不必提供智能指针,并且调用非常直观,没有任何样板。
  • foo可以有多个不同类型的可变长度列表:

    void foo2(
        any_reference_initializer_list<SomeType1> args1,
        any_reference_initializer_list<SomeType2> args2);
    
  • 动态长度(运行时决定的)集合也可以稍作修改(使用std::vector代替std::initializer_list

使用过的课程:

class Base 
{
    virtual void virtFunc() = 0;
};

class Derived1 : public Base
{
    virtual void virtFunc()
    {
        std::cout << "Derived1" << std::endl;
    }
};

class Derived2 : public Base
{
    virtual void virtFunc() 
    {
        std::cout << "Derived2" << std::endl;
    }
};

void doStuffWithBaseRef(Base& b)
{
    b.virtFunc();
}

修改

Mike Kinghan建议使用any_reference_wrapper可能导致难以调试的错误:如果某个函数将any_reference_wrapper作为参数然后继续将其视为一个参数标准std::reference_wrapper(不记得它可能是临时的包装)然后程序可能会崩溃,当临时已经死了但程序的某些部分仍然保留对它的引用。

我的回答是,这个问题对于any_reference_wrapper来说并不是唯一的。在隐式T&&const T&转换的情况下,这种情况也会发生(正如他也指出的那样)。但在这种情况下,正如他所说的那样,一个明确的void f(D const &&) = delete;可以挽救这一天,不允许临时的人作为争论。好的。但我们对any_reference_wrapper的案例有一个类似的解决方案。我们不会使用= delete函数重载该函数,而不是使用某些any_reference_wrapper声明。我们只创建一个接受标准std::reference_wrapper的版本。这样我们就可以确定我们不必处理临时工。

让我重新说一下。如果你在单参数的情况下做了显式删除(因为函数做了一些你不应该对临时工做的事情,比如存储引用),那么根本就不定义重载 {{ 1}},仅void f(std::initializer_list<any_reference_wrapper<const T>>)(请注意后者中的void f(std::initializer_list<std::reference_wrapper<const T>>))。只有当您的函数是安全的时(意味着如果它是单参数的情况,您将允许隐式的std::T&&转换),那么您才应该定义const T&重载。这种情况完全类似于单参数情况,因此它比现代C ++的既定标准行为更容易出错。

答案 2 :(得分:0)

而不是再次宣传我的第一个答案,而不是另一个答案。

我赞赏你有类似程序员的想法而不是为了解决这个问题 它完成了#34;赶走他们。

在您的新解决方案中,any_reference_wrapper<T>大致是合并的 std::reference_wrapper<T>与之前的rvalue_reference_wrapper<T>

有鉴于此,我将讨论rvalue_reference_wrapper<T>的问题 我之前没有追求,这同样是any_reference_wrapper<T>的一个问题 它源于rvalue_reference_wrapper<T>std::reference_wrapper<T>之间的关键区别 std::reference_wrapper<T>

当然,关键的区别在于doSomething({std::ref(A()), std::ref(B()), std::ref(C())}) 无法将其包装引用绑定到临时,这是您的限制 不喜欢,因为它会要求 &#34;笨拙的客户端代码,如#include <functional> struct X{}; int main() { X & rx = std::ref(X()); return 0; } &#34;。

实际上,这样的客户端代码不仅仅是笨拙,而且不会编译。 尝试:

std::ref(const T&&)

您将看到如下诊断信息:

  

错误:使用已删除的函数&#39; void std :: ref(const _Tp&amp;&amp;)[with _Tp = X]&#39;

documentation of std::ref 确认标准图书馆的作者已明确选择 删除std::reference_wrapper(T&&)。他们必然会做出这样的选择,因为 他们还选择明确删除constructor rvalue_reference_wrapper<T> - 你带回来的那个 any_reference_wrapper<T>并保留在any_reference_wrapper<T>。 你正在设计一些不仅仅是缺少的东西 标准库,但在标准库中标示不要这样做。 你正在设计核心语言已经达到的目的。

在这些选择中,图书馆的作者普遍支持这一观点 将临时对象绑定到引用的已批准原则 蛮干。它可以在狭窄的约束条件下安全地完成, 但编写具有目的的类是 令人遗憾。

将临时工具绑定到引用是蛮干的,因为引用可以 在受到约束的临时死亡之后很容易继续使用。

所以,如果您的rvalue_reference_wrapper<T>(或您的#include <iostream> #include <functional> template<class T> class any_reference_wrapper { public: any_reference_wrapper(T && t) : ref(t) {} any_reference_wrapper(T & t) : ref(t) {} operator T &() const {return ref;} T & get() const {return ref;} private: T & ref; }; struct B { virtual void virt() const = 0; virtual ~B() { std::cout << "~B()" <<std::endl; } }; struct D : public B { D(){} void virt() const override { std::cout << "D::virt()" << std::endl; } ~D() override { std::cout << "~D()" <<std::endl; } }; D const & pick_one( std::initializer_list<any_reference_wrapper<D const>> args) { return *args.begin(); // <- Whatever } int main() { D d1, d2; // <- From somewhere D const & rb = pick_one({d1,d2}); rb.virt(); return 0; } ) 曾经一直使图书馆的实际生产代码行使用,并使用 基本上就像下面的程序会出现,但更多 复杂而模糊的方式:

D::virt()
~D()
~B()
~D()
~B()

这一切都很好!输出:

main

几乎完全相同的用途会产生,基本上就是改变了 D d1; // <- From somewhere D const & rb = pick_one({D(),d1}); 中的前两行:

~D()
~B()
pure virtual method called
terminate called without an active exception
Aborted

但是以一些更复杂和模糊的方式。

现在输出是:

pure virtual method called

许多专业的C ++程序员会对什么感到困惑 一种bug可能会导致D崩溃 虚方法调用对没有纯虚拟的类型D const & f(D const & d) { return d; } D const & rb = f(D()); rb.virt(); 的引用 方法,并且在追踪它时可能会感到厌倦和情绪激动。在这个例子中,幸运的是,析构函数跟踪是一个强有力的线索:它是你通过引用绑定一个已经被破坏的临时函数进行虚拟方法调用所能获得的。

确实,可以通过以下方式证明完全相同的漏洞:

any_reference_wrapper<T>

void f(D const &&) = delete; 无处可见。 但我可以关闭它 漏洞:

std::reference_wrapper( T&& x ) = delete;
void std::ref(const T&&) = delete;

与标准库用以下方式关闭它的方式相同:

any_reference_wrapper<T>

另一方面,一目了然arg旨在此漏洞,因此它不会 允许在合格的手中使任何生产代码行的图书馆无法使用。

现在,只要你不坚持将临时工具绑定到foo({arg,[arg,...]}中 你喜欢的std::reference_wrapper<T>模拟的变量函数成语, foo({a,b,c})将符合您规定的避免目标的目标 奇怪的混淆可变函数模板习惯用法 语言,你的客户端编码器可以自由编写函数调用 foo(a,b,c)传统的可变函数习语会强制使用B 他们写virtual int B::value() const = 0;

但是要知道可变参数模板设备是核心 现代C ++无与伦比的表现力,以及所有用途 该设备涉及怪异的混淆应用程序 编译时递归。当C ++首次成为主流 - 在模板之前 - 多态性的设备被认为是如此奇怪和令人困惑 一些C程序员,他们开始发明类可以的方式 在C语言中进行模拟。后来当C ++获得模板时,他们感到困惑 人们非常喜欢用预处理器来模拟模板 宏。变量模板和编译时递归是新常态并具有 好几年了。如果您更愿意设计排序方法来模拟它们 不使用它们你只是要编写奇怪的代码。更好地利用你的大脑 继续学习而不是避免学习。

可变参数模板设备也是无与伦比性能的核心 现代C ++。你可以制作这样的实验程序:

使用纯虚拟成员编写多态基类D

B

编写一个从S

派生的具体课程int variadic() { return 0; } template<class ...T> int variadic(B const & first, T const &... rest) { int i = first.value(); if (sizeof...(rest)) { i += variadic(rest...); } return i; } (或多个)

通过您的选择生成一些派生对象的序列int pseudo_variadic(std::initializer_list<std::reference_wrapper<B const>> args) { int i = 0; for (B const & arg : args) { i += arg.value(); } return i; }

定义传统的可变参数函数模板:

variadic

定义伪变量函数:

pseudo_variadic

所以这里你有一个可变函数和一个伪可变参数函数 使用他们所有的参数,实现相同的算法。在每种情况下, 这涉及从头到尾迭代参数。对于S, 迭代在递归中以在编译时完成。对于clock() 它在运行时循环完成。

在循环中调用每个函数,比如一百万次,传递每个参数 在-O2中以适当的方式,并确保调用的结果 必须由程序使用。

使用variadic计算每个循环的时间并进行比较。通过pseudo_variadic优化 使用gcc 4.9,我发现pseudo_variadic平均执行速度快4.3倍 比{{1}}。这并不令人惊讶,因为{{1}} 代表了一种使用C ++实现可变函数的方法 perl或python的方式。

答案 3 :(得分:0)

您可以使用这些宏

typedef int INT_ARRAY[];
#define BEGIN_FOR_EACH_VARIADIC(ArgT,ArgV,param) INT_ARRAY{([&](ArgT param)
#define END_FOR_EACH_VARIADIC(ArgT,ArgV) (std::forward<ArgT>(ArgV)),0)...,0};

然后使用可变参数模板:

template <class...Ts>
void do_foo(Ts...Args) {
   BEGIN_FOR_EACH_VARIADIC(Ts,Args,param) 
   {
       param.virtFunc();
       doStuffWithBaseRef(param);
   }
   END_FOR_EACH_VARIADIC(Ts,Args)
}

非常简单。如果要强制传递特定类型,只需使用包装器。

template<class...Ts>
void foo(Ts...Args) 
{return do_foo(((const Base&)Args)...);}