【问题标题】: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 中的容器)

标签: c++ c++11 initializer-list


【解决方案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] = {double{1}, double{2}, double{3}};
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[];
    expander{0, (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://stackoverflow.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();
    

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

    【讨论】:

      猜你喜欢
      • 2017-08-25
      • 1970-01-01
      • 1970-01-01
      • 2017-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多