繁体   English   中英

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

[英]Variable number of (const) reference arguments

我试图消除我的高级代码中的原始指针(使用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()})其中 ABC是从 T派生的类。

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

使用可变参数模板函数执行此操作。 当然不需要将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在一般情况下的定义并不限制所有参数都是(可转换为) 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函数 )并且你还没有发明它。 如果函数是B)类型,那么显然函数可以迭代作为函数参数的C 成员T s。 这就是你在答案中在foo中所做的事情。

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

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

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

将在你的答案中按照fooDerived1Derived2的定义编译和运行,更有可能的情况:

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)});

所以,现在,您正在构建“arguments”的临时副本 ,以及rvalue_reference_wrapper对临时副本的引用的initalizer_list ,以便您可以迭代对foo临时对象的引用。

好吧,如果你必须使用“参数”的副本,那么打扰构建一系列对副本引用是多余 只需将“参数”复制到任何合适的容器中,并将[const]引用传递给foo 这不会阻止foo迭代[const]对容器成员的引用,就像现在一样。

你似乎可能部分地通过以下问题来运用: 对于从多态基 B 派生的各种类型的对象 ,如果不是动态分配对象的原始指针的容器, 什么是合适的容器

一个毫无争议的答案是: std:: Container <std::shared_ptr<B>> ,其中Container是标准容器模板(向量,列表等),为您的应用程序提供适当的接口。 更一般地说,所谓的智能指针模板, std::shared_ptr<T>文档 )和std::unique_ptr<T>文档 )是用于避免暴露原始动态指针的标准C ++ 11资源。

您似乎也被std::initializer_list吸引,因为您可以使用支撑的初始化程序轻松地在使用点构建一个可迭代序列。 可以保留这种便利,而无需处理原始动态指针智能指针。 例如

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});

会很好,所以会:

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

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

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

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我们有一个类似的解决方案。 我们只是不重载函数来接受any_reference_wrapper而不是使用some = delete声明。 我们只创建一个接受标准std::reference_wrapper的版本。 这样我们就可以确定我们不必处理临时工。

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

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

我赞赏你有类似程序员的想法,而不是为了解决它们的“这是怎么做”。

在新的解决方案中, 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>

当然,关键的区别在于std::reference_wrapper<T>无法将其包装引用绑定到临时,这是您不喜欢的限制,因为它会调用“像doSomething({std::ref(A()), std::ref(B()), std::ref(C())})这样笨拙的客户端代码doSomething({std::ref(A()), std::ref(B()), std::ref(C())}) “。

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

#include <functional>
struct X{};

int main()
{
    X & rx = std::ref(X());
    return 0;
}

你会看到像这样的诊断:

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

std::ref文档确认标准库的作者选择明确删除std::ref(const T&&) 他们必然会做出这样的选择,因为他们还选择明确删除构造函数 std::reference_wrapper(T&&) - 你带回来的rvalue_reference_wrapper<T>并保存在any_reference_wrapper<T> 您正在设计一些不仅仅是标准库中缺少的东西,而是在标准库中标记为“不要这样做 而你正在设计核心语言已经实现的目的。

在这些选择中,图书馆的作者坚持普遍认可的原则,即将临时对象绑定到引用是蛮干的。 它可以完成并且可以在狭窄的约束内安全地发生,但是为了实现它而编写类是令人遗憾的。

将临时工具绑定到引用是蛮干的,因为在临时绑定之后,引用可以如此容易地继续使用。

因此,如果你的any_reference_wrapper<T> (或你的rvalue_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

许多专业的C ++程序员会因为什么样的bug可能会导致一个pure virtual method called crash的pure virtual method called从一个类型D的引用上的pure virtual method called崩溃而变得神秘, 它没有纯粹的虚方法 ,并且可能因为他们追踪它的时间。 在这个例子中,幸运的是,析构函数跟踪是一个强有力的线索:这是你通过引用绑定一个已经被销毁的临时函数来进行虚拟方法调用的结果。

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

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>设计为具有此漏洞,因此不允许在合格的手中使任何生产代码行的库无法使用。

现在,只要你不坚持将foo({arg,[arg,...]}模拟的变量函数习惯用法中的arg s绑定临时值, std::reference_wrapper<T>就会满足你的要求避免语言本机的奇怪混乱的可变参数函数模板习惯用法的目标,你的客户端编码器可以自由编写函数调用,如foo({a,b,c}) ,其中传统的可变参数函数习惯用法强制它们写foo(a,b,c)

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

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

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

virtual int B::value() const = 0;

写一个从B派生的具体类D (或多个)

通过您的选择生成一些派生对象的序列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使用gcc 4.9的优化,我觉得variadic ,平均执行速度比4.3倍pseudo_variadic 这并不pseudo_variadic ,因为pseudo_variadic代表了一种使用C ++以perl或python的方式实现可变参数函数的方法。

您可以使用这些宏

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)...);}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM