На основе диапазона используется с инициализатором скобок по неконстантным значениям?

Я пытаюсь выполнить итерацию по нескольким 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.

Я правильно догадываюсь, что скот-инициализатор создает копию этих списков? И есть ли способ не копировать их и сделать их модифицируемыми внутри цикла? (кроме создания списка указателей на них, что является моим текущим временным решением).

Ответы

Ответ 1

Вы догадываетесь правильно. Элементы std::initializer_list всегда const (что делает невозможным sort() невозможность, поскольку sort() является функцией члена не const), и ее элементы всегда копируются (что сделало бы sort() -извлечение их бессмысленным даже если они не были const). Из [dcl.init.list] акцент мой:

Объект типа std::initializer_list<E> создается из списка инициализаторов, как если бы реализация выделен временный массив из N элементов типа const E, где N - количество элементов в список инициализаторов. Каждый элемент этого массива копируется-инициализирован с соответствующим элементом инициализатора список, а объект std::initializer_list<E> создан для обращения к этому массиву. [Примечание: конструктор или функция преобразования, выбранная для копии, должна быть доступна (раздел 11) в контексте инициализатора список. -end note] Если для инициализации любого из элементов требуется сужение преобразования, программа плохо сформирован. [Пример:

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

потому что это указатель, который const, а не тот элемент, на который он указывает. Другой альтернативой было бы написать шаблон вариационной функции:

template <typename... Lists>
void sortAll(Lists&&... lists) {
    using expander = int[];
    expander{0, (void(lists.sort()), 0)...};
}

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()

Ответ 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://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();
    }
}

Ответ 3

Синтаксис {...} фактически создает std::initializer_list. Как указано в связанной странице:

Объект A std::initializer_list автоматически создается, когда:

  • [...]
  • бит-init-list привязан к auto, в том числе в цикле для диапазона

И:

Объектом типа std::initializer_list<T> является легкий прокси-объект, который обеспечивает доступ к массиву объектов типа const T.

Таким образом, вы не можете изменять объекты, к которым обращались через этот initialize_list. Ваши решения с указателями выглядят как самые простые для меня.

Ответ 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