将一个 vector<T> 转换为 initializer_list<T>

85

每个人都可以从 std::initializer_list 创建 std::vector,但是反过来呢?

例如,如果您将 std::initializer_list 用作参数:

void someThing(std::initializer_list<int> items)
{
...
}

有时候你会把你的物品放在一个vector<T>中而不是字面上的列表中:

std::vector<int> v;
// populate v with values
someThing(v); // boom! No viable conversion etc.

更一般的问题是:如何从STL可迭代对象中创建一个std::initializer_list,而不仅仅是std::vector


我也想要这个,只是想传递一个常量数组而不是向我的重载传递向量。很荒谬的是 foo({1,2,3}) 能够正常工作,但是 int f[] = {1,2,3}; foo(f); 却不能;而且需要传递双迭代器,这正是我想要避免的,因为 std::initializer_list 如此巧妙地将两者包装成一个。至少,我希望有一个 std::initializer_list 构造函数可以接受一个 const 静态数组。 - Dwayne Robinson
一个有用的用例是,我有一个返回vector作为临时对象的方法,并且我想从中创建一个set。如果我可以写成set<Type> foo{initializer_list<Type>(getVector())};(或类似的东西),那就可以了。我不能写成set<Type> foo(getVector.begin(), getVector().end());(返回值没有被存储,而是在调用期间计算)。所以我需要将向量复制到本地对象中,并使用该对象进行迭代器。这是额外的移动构造和更多的代码行,以及意外引用的额外变量。 - Troy Daniels
一个initializer_list不仅仅是包装两个迭代器,一个重要的细节是内存是连续的。现在使用vector就是这种情况,但是一旦你获得了迭代器,即使它是随机迭代器,连续性方面也会丢失。请参见下面的答案,了解什么是有效的,但有点未经记录。 - QBziZ
7个回答

42
答案是否定的,您不能这样做。
类型为std::initializer_list<T>的对象是一个轻量级代理对象,提供对类型为T的对象数组的访问。当以下情况时,会自动构造一个std::initializer_list对象:
  • 使用花括号初始化列表进行列表初始化,包括函数调用列表初始化和赋值表达式(不要与构造函数初始化列表混淆)
  • 使用花括号初始化列表绑定到auto,包括在范围for循环中
就库支持而言,std::initializer_list只有一个默认构造函数,它构造一个空列表,其迭代器是常量的。缺少push_back()成员意味着您不能使用std::copystd::back_inserter迭代器适配器来填充它,也不能直接通过这些迭代器进行赋值。
#include <algorithm>
#include <initializer_list>
#include <iterator>
#include <vector>

int main() 
{
    auto v = std::vector<int> { 1, 2 };
    std::initializer_list<int> i;
    auto it = std::begin(i);
    *it = begin(v); // error: read-only variable is not assignable
}

示例演示

如果你查看标准容器,除了在构造函数/插入函数中接受std::initializer_list之外,它们都有接受迭代器对的构造函数/插入函数。实现可能会将initializer_list函数委托给相应的迭代器对函数。例如,libc++中std::vector<T>::insert函数就是这个简单的一行代码:

 iterator insert(const_iterator __position, initializer_list<value_type> __il)
        {return insert(__position, __il.begin(), __il.end());}

你需要按照类似的方式修改你的代码:
```html

你应该按照类似的方式修改你的代码:

```
void someThing(std::initializer_list<int> items)
{
    someThing(items.begin(), items.end()); // delegate
}

template<class It>
void someThing(It first, It last)
{
    for (auto it = first, it != last; ++it) // do your thing
}

当您将项目存储在向量而不是文字列表中时:

std::vector<int> v = { 1, 2 };
auto i = { 1, 2 };
someThing(begin(v), end(v)); // OK
someThing(i); // also OK
someThing({1, 2}); // even better

是的,我已经得出了传递双迭代器的结论 - 我想我对STL中的IEnumerable<T>(来自.NET世界)的等效物感到困惑。没有办法传递单个迭代器,并知道你已经到达了末尾,对吧? - Fil
@Fil 不,那是不可能的。Ranges 能够检测它们的结束。 - TemplateRex
1
std::initializer_list<int> i(v.data(), v.data() + v.size()); 看起来可以编译并作为参数传递。 - Dwayne Robinson
4
@Dwayne,它可以工作(并且在大多数编译器上可能会工作),但标准规定initializer_list只有默认构造函数。 - magras
@DwayneRobinson,执行该操作时出现以下错误:error: 'constexpr std::initializer_list<_E>::initializer_list(std::initializer_list<_E>::const_iterator, std::initializer_list<_E>::size_type) [with _E = d2s::Json::FileRecord; std::initializer_list<_E>::const_iterator = const d2s::Json::FileRecord*; std::initializer_list<_E>::size_type = long unsigned int]' is private within this context - Michael
@Michael 是的,根据magras的评论,它似乎在一些编译器上工作,但并非标准。 - Dwayne Robinson

10

显然不行,这是不可能的。没有这样的构造函数(我相信有很好的理由),std::initializer_list是一个奇怪的生物。

相反,您可以将someThing()更改为接受一对迭代器。这样,只要您可以更改该函数的签名(它不在第三方库中等等),您就可以得到想要的结果。


5

是的,你可以这样做,但你不想这样做,因为你必须这样做的方式非常愚蠢。

首先,确定你的列表的最大长度。由于size_t并不是无限制的,所以必须有一个最大长度。理想情况下,找到一个更好(更小)的长度,比如10。

其次,编写魔术开关代码,将运行时整数映射到编译时整数,然后使用该编译时整数调用模板类或函数。这种代码需要一个最大整数大小--使用上面的最大长度。

现在,将向量的大小魔术开关为编译时长度。

0length-1创建一个编译时整数序列。在initializer_list构造中解包该序列,每次在std::vector上调用[]。使用结果的initializer_list调用你的函数。

以上方法很棘手,很荒谬,大多数编译器都会崩溃。我不确定其中一步的合法性--在构造initializer_list时是否可以进行可变参数解包?

以下是魔术开关的一个示例:Can I separate creation and usage locations of compile-time strategies?

以下是索引或序列技巧的示例:Constructor arguments from tuple

这篇文章只应该具有理论意义,因为实际上这是解决这个问题的一种非常愚蠢的方法。

使用任意可迭代对象进行此操作更加困难,而不进行n^2的工作。但由于上述方法已经够荒谬了,任意可迭代版本会更加荒谬...(也许可以用一个lambda包--使参数按顺序计算可能会很棘手。在初始化列表的各个参数之间是否存在一个序列点?)


似乎没有人会在库中要求 std::initializer_list<T>,而是使用双迭代器。这也很好 :) - Fil

2

我曾发布过一种看起来可行的方法,但由于initializer_lists被视为对本地作用域值的引用而导致内存访问违规,所以不幸地失败了。

这里有一个替代方案。针对每个可能的项数,使用参数包进行计数,生成一个单独的函数和一个单独的静态初始化列表。这不是线程安全的,并且使用const_cast(被认为非常糟糕)来写入静态初始化列表内存。然而,在gcc和clang中都可以干净地工作。

如果由于某种模糊原因您需要解决此问题并且没有其他选择,那么可以尝试此黑客技巧。

#include <initializer_list>
#include <iostream>
#include <stdexcept>
#include <type_traits>
#include <vector>

namespace __range_to_initializer_list {

    constexpr size_t DEFAULT_MAX_LENGTH = 128;

    template <typename V> struct backingValue { static V value; };
    template <typename V> V backingValue<V>::value;

    template <typename V, typename... Vcount> struct backingList { static std::initializer_list<V> list; };
    template <typename V, typename... Vcount>
    std::initializer_list<V> backingList<V, Vcount...>::list = {(Vcount)backingValue<V>::value...};

    template <size_t maxLength, typename It, typename V = typename It::value_type, typename... Vcount>
    static typename std::enable_if< sizeof...(Vcount) >= maxLength,
    std::initializer_list<V> >::type generate_n(It begin, It end, It current)
    {
        throw std::length_error("More than maxLength elements in range.");
    }

    template <size_t maxLength = DEFAULT_MAX_LENGTH, typename It, typename V = typename It::value_type, typename... Vcount>
    static typename std::enable_if< sizeof...(Vcount) < maxLength,
    std::initializer_list<V> >::type generate_n(It begin, It end, It current)
    {
        if (current != end)
            return generate_n<maxLength, It, V, V, Vcount...>(begin, end, ++current);

        current = begin;
        for (auto it = backingList<V,Vcount...>::list.begin();
             it != backingList<V,Vcount...>::list.end();
             ++current, ++it)
            *const_cast<V*>(&*it) = *current;

        return backingList<V,Vcount...>::list;
    }

}

template <typename It>
std::initializer_list<typename It::value_type> range_to_initializer_list(It begin, It end)
{
    return __range_to_initializer_list::generate_n(begin, end, begin);
}

int main()
{
    std::vector<int> vec = {1,2,3,4,5,6,7,8,9,10};
    std::initializer_list<int> list = range_to_initializer_list(vec.begin(), vec.end());
    for (int i : list)
        std::cout << i << std::endl;
    return 0;
}

1
“对我不起作用”,尽管我不确定您依赖的是哪种未定义行为。 - TemplateRex
1
这可能是 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56568 。clang 也不喜欢这个。我认为我们现在不应该从函数返回 initializer_lists。 - fuzzyTew
已更新以解决内存和作用域限制的问题。 - fuzzyTew
名称 __range_to_initializer_list 被保留给语言实现。 - eerorika
鉴于这会改变编译时结构的内容,使用保留名称是合适的。如何或是否实现此功能是技术上定义的。最好将此想法与Yakk的想法混合在一起,从引用中生成初始化列表。 - fuzzyTew

0

如果你不介意复制,那么我认为这样做可以解决问题:

template<class Iterator>
using iterator_init_list = std::initializer_list<typename std::iterator_traits<Iterator>::value_type>;

template<class Iterator, class... Ts>
iterator_init_list<Iterator> to_initializer_list(Iterator start, Iterator last, Ts... xs)
{
    if (start == last) return iterator_init_list<Iterator>{xs...};
    else return to_initializer_list(start+1, last, xs..., *start);
}

有趣的想法,但不幸的是我相当确定这是未定义行为。initializer_list不是可以从函数返回的通用容器;它们的生命周期与局部变量一样受到作用域的限制,并且不能保证initializer_list中的对象在调用之外会持续存在(https://dev59.com/EOo6XIcBkEYKwwoYORsV#17635374)。 - Human-Compiler

0

我认为,为了通过使用返回迭代器的两种方法传递向量而不使用模板化的晦涩迭代器类的最佳解决方案就是在一个接受向量的函数中实现您的函数逻辑。

void someThing(std::initializer_list<int> items)
{
     std::vector<int> v;
     for(int i:items)
     {
             v.push_back(i);
     }
     someThing(v);
}

void someThing(std::vector<int> items)
{
...
}

-1
std::vector<int> v;
someThing(std::initializer_list<int>(&v.front(), &v.front() + v.size())); 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接