基于范围的大括号初始化器而不是非常量值?
Posted
技术标签:
【中文标题】基于范围的大括号初始化器而不是非常量值?【英文标题】:Range-based for with brace-initializer over non-const values? 【发布时间】:2015-10-21 21:00:09 【问题描述】:我正在尝试遍历多个std::list
s,并对它们中的每一个进行排序。这是天真的方法:
#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 : &a, &b, &c) l->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<E>
类型的对象是从初始化列表构造的,就像实现一样 分配了一个由 const E 类型的 N 个元素组成的临时数组,其中 N 是 初始化列表。该数组的每个元素都与初始值设定项的相应元素一起复制初始化 列表,并构造std::initializer_list<E>
对象以引用该数组。 [注:构造函数 为副本选择的或转换函数应在初始化程序的上下文中可访问(第 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_wrapper
到list<int>
的数组(因为你不能有一个引用数组),但这可能更令人困惑而不是有用:
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
应用于原始a
、b
和c
,我们需要(非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
引用,您也将修改a
、b
和c
的副本而不是原件。
不管怎样,这里是ref_range
。它构建了vector
的reference_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
。正如链接页面所述:
[...] braced-init-list 绑定到
std::initializer_list
对象在以下情况下自动构造:auto
,包括在一个范围内的 for 循环中
还有:
std::initializer_list<T>
类型的对象是一个轻量级代理对象,它提供对const T
类型对象数组的访问。
因此,您无法修改通过此initialize_list
访问的对象。您的带有指针的解决方案对我来说似乎是最简单的解决方案。
【讨论】:
在这个问题的其他地方,你说初始化列表不复制它的项目。但是我在这个例子中检查过,基于范围的项目的地址与原始a
,b
,c
的地址不同
@AaronMcDaid 是的,我编辑了与我之后发现的相同的评论。 “底层数组是一个临时数组,其中每个元素都是从原始初始化列表的相应元素复制初始化的(除了缩小转换无效)。”【参考方案4】:
直接回答您的问题:
我是否正确猜测大括号初始化器正在创建 那些名单?
是的,这是第一个问题。您的代码会创建列表的副本,对这些副本进行排序,最后忘记排序后的副本。
但是,仅此一项只会导致代码无法工作。编译器错误提示第二个问题:l
的隐式类型是list<int> const&
,而不是list<int>&
。所以编译器抱怨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();
当然,这与已经建议的指针解决方案非常相似。
【讨论】:
以上是关于基于范围的大括号初始化器而不是非常量值?的主要内容,如果未能解决你的问题,请参考以下文章