基于范围的大括号初始化器而不是非常量值?

Posted

技术标签:

【中文标题】基于范围的大括号初始化器而不是非常量值?【英文标题】:Range-based for with brace-initializer over non-const values? 【发布时间】:2015-10-21 21:00:09 【问题描述】:

我正在尝试遍历多个std::lists,并对它们中的每一个进行排序。这是天真的方法:

#include<list>
using namespace std;
int main(void)
    list<int> a,b,c;
    for(auto& l:a,b,c) l.sort();

生产

aa.cpp:5:25: error: no matching member function for call to 'sort'
        for(auto& l:a,b,c) l.sort();
                             ~~^~~~
/usr/bin/../lib64/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/stl_list.h:1586:7: note: 
      candidate function not viable: 'this' argument has type 'const
      std::list<int, std::allocator<int> >', but method is not marked const
      sort();
      ^
/usr/bin/../lib64/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/stl_list.h:1596:9: note: 
      candidate function template not viable: requires 1 argument, but 0 were
      provided
        sort(_StrictWeakOrdering);
        ^
1 error generated.

我是否正确猜测大括号初始化器正在创建这些列表的副本?有没有办法不复制它们,并使它们在循环内可修改? (除了列出指向它们的指针之外,这是我目前的解决方法)。

【问题讨论】:

有点丑,但也许for (auto l : &amp;a, &amp;b, &amp;c) l-&gt;sort(); @Quentin,initializer_list 是原始a,b,c 的副本列表。因此,即使我们可以获得非const 引用,它也不会很有用。我们需要一个存储引用的 initializer_list(或类似容器)。我认为这是不可能的,除非您构建自己的容器(或者使用 Boost 中的容器) 【参考方案1】:

你猜对了。 std::initializer_list 元素总是 const (这使得 sort() 不可能,因为 sort() 是非 const 成员函数)并且它的元素总是被复制(这会使 sort()-ing 它们毫无意义即使他们不是const)。来自 [dcl.init.list],强调我的:

std::initializer_list&lt;E&gt; 类型的对象是从初始化列表构造的,就像实现一样 分配了一个由 const E 类型的 N 个元素组成的临时数组,其中 N 是 初始化列表。该数组的每个元素都与初始值设定项的相应元素一起复制初始化 列表,并构造 std::initializer_list&lt;E&gt; 对象以引用该数组。 [注:构造函数 为副本选择的或转换函数应在初始化程序的上下文中可访问(第 11 条) 列表。 —结束注释] 如果需要进行缩小转换来初始化任何元素,则程序是 格式不正确。 [示例:

struct X 
    X(std::initializer_list<double> v);
;
X x 1,2,3 ;

初始化将以大致相当于这样的方式实现:

const double __a[3] = double1, double2, double3;
X x(std::initializer_list<double>(__a, __a+3));

假设实现可以用一对指针构造一个initializer_list 对象。 ——结束 例子]

没有办法使它们成为非常量或非复制。指针解决方案有效:

for (auto l : &a, &b, &c) l->sort();

因为 pointer 是 const,而不是它指向的元素。另一种选择是编写可变参数函数模板:

template <typename... Lists>
void sortAll(Lists&&... lists) 
    // before C++17
    using expander = int[];
    expander0, (void(lists.sort()), 0)...;

    // C++17 or later
    (lists.sort(), ...);


sortAll(a, b, c);

你也可以,我猜,写一个助手来将你的列表包装成一个reference_wrapperlist&lt;int&gt;的数组(因为你不能有一个引用数组),但这可能更令人困惑而不是有用:

template <typename List, typename... Lists>
std::array<std::reference_wrapper<List>, sizeof...(Lists) + 1>
as_array(List& x, Lists&... xs) 
    return x, xs...; 


for (list<int>& l : as_array(a, b, c))   // can't use auto, that deduces
    l.sort();                             // reference_wrapper<list<int>>,
                                         // so would need l.get().sort()

【讨论】:

在答案中应该更清楚地表明编译器错误是由隐式const引起的,并且列表的复制是与编译器错误无关的附加问题. @vog,从某种意义上说,复制实际上是这段代码的真正问题。为了使sort 应用于原始abc,我们需要(非const)对原始对象的引用。也许我们可以说这就是成员是 const 的原因,以避免给人一种修改它们会很有用的印象。 @AaronMcDaid 我完全同意。我只是说答案应该区分真正的问题和编译器抱怨的问题,即使这两者是相关的。 另一种解决方案是可变参数函数,它返回引用数组,类似于std::tie。这可能更通用。 @Slava,但在 C++ 中无法使用引用数组。但是你是对的,可以构建一个更灵活的容器。 (实际上,我现在正在尝试编写代码!)【参考方案2】:

可以编写一个函数ref_range,让你这样做:

for(auto& l : ref_range(a,b,c)) 
    l.sort();

正如其他人所说,一旦你写了a,b,c,你就会被initializer_list 困住,这样的列表总是会复制它的参数。副本是const(因此您的错误),但即使您可以获得非const 引用,您也将修改abc 的副本而不是原件。

不管怎样,这里是ref_range。它构建了vectorreference_wrapper

// http://***.com/questions/31724863/range-based-for-with-brace-initializer-over-non-const-values
#include<list>
#include<functional>
#include<array>

template<typename T, std:: size_t N>
struct hold_array_of_refs 
    using vec_type = std:: array< std:: reference_wrapper<T>, N >;
    vec_type m_v_of_refs;
    hold_array_of_refs(vec_type && v_of_refs) : m_v_of_refs(std::move(v_of_refs))  
    ~hold_array_of_refs()  
    struct iterator 
        typename vec_type :: const_iterator m_it;
        iterator(typename vec_type :: const_iterator it) : m_it(it) 
        bool operator != (const iterator &other) 
            return this->m_it != other.m_it;
        
        iterator& operator++()  // prefix
            ++ this->m_it;
            return *this;
        
        T& operator*() 
            return *m_it;
        
    ;

    iterator begin() const 
        return iterator(m_v_of_refs.begin());
    
    iterator end() const 
        return iterator(m_v_of_refs.end());
    
;

template<typename... Ts>
using getFirstTypeOfPack = typename std::tuple_element<0, std::tuple<Ts...>>::type;


template<typename ...T>
auto ref_range(T&... args) -> hold_array_of_refs< getFirstTypeOfPack<T...> , sizeof...(args)> 
    return  std:: ref(args)... ; // Why does clang prefer three levels of  ?


#include<iostream>
int main(void)
    std:: list<int> a,b,c;
    // print the addresses, so we can verify we're dealing
    // with the same objects
    std:: cout << &a << std:: endl;
    std:: cout << &b << std:: endl;
    std:: cout << &c << std:: endl;
    for(auto& l : ref_range(a,b,c)) 
        std:: cout << &l << std:: endl;
        l.sort();
    

【讨论】:

您使用std::vector 有什么特别的原因吗?如您所知,可以使用编译时的大小std::array,或者我遗漏了什么? 已更改为array,感谢@Slava。并稍微简化了 ref_range。但我对 clang 有点惊讶,它建议我在来自ref_range的返回语句中添加第三组大括号@ 这些可以是constexpr(以启用循环展开等优化)吗? @BenVoigt,好建议。我现在正在尝试,虽然operator++ 有问题。即使打开 C++14 也无济于事。不过会继续努力 @BenVoigt,got it working 与 constexpr。不得不做出一些改变,包括实现我自己的reference_wrapper。 Clang 3.5 有效,但 g++ 4.9.3 无效。 Coliru 上的 g++ 5.2.0 可以正常工作【参考方案3】:

... 语法实际上是在创建一个std::initializer_list。正如链接页面所述:

std::initializer_list 对象在以下情况下自动构造:

[...] braced-init-list 绑定到 auto,包括在一个范围内的 for 循环中

还有:

std::initializer_list&lt;T&gt; 类型的对象是一个轻量级代理对象,它提供对const T 类型对象数组的访问。

因此,您无法修改通过此initialize_list 访问的对象。您的带有指针的解决方案对我来说似乎是最简单的解决方案。

【讨论】:

在这个问题的其他地方,你说初始化列表不复制它的项目。但是我在这个例子中检查过,基于范围的项目的地址与原始a,b,c的地址不同 @AaronMcDaid 是的,我编辑了与我之后发现的相同的评论。 “底层数组是一个临时数组,其中每个元素都是从原始初始化列表的相应元素复制初始化的(除了缩小转换无效)。”【参考方案4】:

直接回答您的问题:

我是否正确猜测大括号初始化器正在创建 那些名单?

是的,这是第一个问题。您的代码会创建列表的副本,对这些副本进行排序,最后忘记排序后的副本。

但是,仅此一项只会导致代码无法工作。编译器错误提示第二个问题:l 的隐式类型是list&lt;int&gt; const&amp;,而不是list&lt;int&gt;&amp;。所以编译器抱怨sort() 试图修改常量列表。

您可以使用讨厌的const_cast 来解决第二个问题:

#include <list>
#include <iostream>
using namespace std;
int main(void)
    list<int> a,b,c;
    a.push_back(2);
    a.push_back(0);
    a.push_back(1);
    for(auto& l:a,b,c) const_cast<list<int>&>(l).sort();
    for(auto i:a) cout << i << endl;

但是,这将引发第一个问题:您的列表列表包含副本,并且只有这些副本被排序。所以最终的输出不是你想要的:

2
0
1

最简单的解决方法是创建一个指向列表的指针列表:

#include <list>
#include <iostream>
using namespace std;
int main(void)
    list<int> a,b,c;
    a.push_back(2);
    a.push_back(0);
    a.push_back(1);
    for(auto l:&a,&b,&c) l->sort();
    for(auto i:a) cout << i << endl;

这将产生预期的结果:

0
1
2

【讨论】:

【参考方案5】:

其他人已经提到了std::reference_wrapper,但他们随后用它来创建一个 STL 容器,而不是坚持使用大括号初始化器列表。

所以你需要做的就是:

for(auto& l:std::ref(a),std::ref(b),std::ref(c)) l.get().sort();

当然,这与已经建议的指针解决方案非常相似。

【讨论】:

以上是关于基于范围的大括号初始化器而不是非常量值?的主要内容,如果未能解决你的问题,请参考以下文章

std::array 初始化中的大括号省略

联合中的大括号或相等初始化器

Java中大括号的作用是啥?

忽略 GCC“错误:标量初始化器类型的大括号”错误。让他们警告

SHELL的判断括号区别

mathtype表示分类的大括号怎么打?