Как правильно (но эффективно) реализовать что-то вроде vector::insert? (Псевдоним указателя)

Рассмотрим эту гипотетическую реализацию vector:

template<class T>  // ignore the allocator
struct vector
{
    typedef T* iterator;
    typedef const T* const_iterator;

    template<class It>
    void insert(iterator where, It begin, It end)
    {
        ...
    }

    ...
}

Проблема

Здесь мы сталкиваемся с тонкой проблемой:
существует вероятность того, что begin и end ссылаются на элементы в одном и том же векторе, после where.

Например, если пользователь говорит:

vector<int> items;
for (int i = 0; i < 1000; i++)
    items.push_back(i);
items.insert(items.begin(), items.end() - 2, items.end() - 1);

Если It не является типом указателя, то все в порядке.
Но мы не знаем, поэтому мы должны проверить, что [begin, end) не относится к диапазону, уже находящемуся внутри вектора.

Но как мы это делаем? Согласно C++, если они не ссылаются на один и тот же массив, то сравнение указателей будет неопределенным!
Таким образом, компилятор может ложно сообщить нам, что элементы не являются псевдонимами, хотя на самом деле они это делают, что приводит к ненужному замедлению на O(n).

Возможное решение и предостережение

Одним из решений является копирование полного вектора каждый раз, чтобы включить новые элементы, а затем выбросить старую копию.

Но это очень медленно в таких сценариях, как в приведенном выше примере, где мы копируем 1000 элементов только для того, чтобы вставить 1 элемент, даже если у нас уже может быть достаточно места.

Есть ли общий способ (правильно) решить эту проблему эффективно, то есть без замедления O(n) в случаях, когда ничего не сглаживается?


person user541686    schedule 20.08.2012    source источник
comment
Вы можете использовать предикаты std::less и т. д., которые гарантированно дадут общий порядок, даже если сравнение необработанных указателей этого не сделает.   -  person Mankarse    schedule 20.08.2012
comment
Тем не менее, я почти уверен (я действительно не думал об этой проблеме раньше), что vectors, определенные в стандарте, имеют неопределенное поведение, если вы попытаетесь сделать это, потому что итераторы становятся недействительными insert. (Не препятствовать реализации, которая расширяет/изменяет std::vector для поддержки этого).   -  person Mankarse    schedule 20.08.2012
comment
@Mankarse: Вау, правда?! Значит, less(p1, p2) не занимается УБ, а p1 < p2 занимается?? Я этого не знал!   -  person user541686    schedule 20.08.2012
comment
Ага! [comparisons]/8: For templates greater, less, greater_equal, and less_equal, the specializations for any pointer type yield a total order, even if the built-in operators <, >, <=, >= do not.   -  person Mankarse    schedule 20.08.2012
comment
В лучшем случае вы понесете лишний if. Почему вы все время страдаете от замедления O(n)?   -  person David    schedule 20.08.2012
comment
@Mankarse: Пожалуйста, опубликуйте это как ответ! Это потрясающая находка! :D   -  person user541686    schedule 20.08.2012
comment
@Dave: Ну, если вы скопируете все это, что было моим первоначально предложенным решением, это будет O (n). Как я уже упоминал, наивная реализация if работает некорректно.   -  person user541686    schedule 20.08.2012
comment
@Мехрдад Хммм. Если не хватает места, вы копируете в новую память, и это легко. Если есть, и запрос состоит в том, чтобы вставить часть себя, скопируйте этот раздел в конец данных и замените каждый элемент элементами, начинающимися с того, где. Если емкости достаточно и запрос не состоит в том, чтобы вставить часть самого себя, скопируйте where to end() в конец данных и скопируйте begin to end туда, где. Извините, если то, как я это написал, сбивает с толку, но я бы посчитал то, что я только что написал, наивной if реализацией.   -  person David    schedule 20.08.2012
comment
@Dave: Хм, я не думаю, что обмен работает сам по себе - я думаю, вам понадобится std::rotate, верно?   -  person user541686    schedule 20.08.2012
comment
@Mehrdad std::rotate в основном представляет собой цикл свопов, верно? Я никогда не использовал его. Но на самом деле std::rotate_copy может быть вся логика подкачки, которую я описал, уже написанная для вас.   -  person David    schedule 20.08.2012
comment
@Dave: Верно, но сколько свопов? Мне это кажется O(n), если я правильно смотрю на это. (Вы можете возразить, что это всегда в пределах постоянного коэффициента и т. д., но на практике это все равно намного медленнее, чем должно быть.)   -  person user541686    schedule 20.08.2012
comment
@Mehrdad Ну, это O (n), но O - дерьмовый дескриптор. Вам нужно только пройтись по тому, где to end() и начать заканчиваться в любом случае, если вы не выделяете новую память. Не начинать() туда, где. Как этого избежать? Я думаю, вы можете использовать SSE для оптимизации небольших типов.   -  person David    schedule 20.08.2012
comment
@Dave: О, хм, да, если подумать об этом, вам все равно придется касаться столько же элементов, так что в любом случае это O (n), мой плохой. (Хоть это и свопы, а не копирование, так что медленнее.) Но у меня другой вопрос: Вы сказали, Если есть и запрос вставить кусок самого себя... но как вы тестируете за это в первую очередь?   -  person user541686    schedule 20.08.2012
comment
@Mehrdad Основываясь на том, что вы сказали, в стандарте говорится о сравнении указателей, вы не можете сделать это обычным кросс-платформенным способом. На самом деле вы можете сделать очевидное сравнение указателей, и оно будет работать на любой платформе, известной человеку, но педантично вы не можете догадаться.   -  person David    schedule 20.08.2012
comment
@Dave: Да, это была вся проблема с самого начала. Я не покупаюсь на то, что это будет работать на каждой платформе, из-за (по крайней мере) ошибок подобных этому. Также поймите, что если компилятор каким-то образом может сказать, что единственное значение, которое вы когда-либо присваиваете указателю, является началом или концом массива, то он всегда может оптимизировать сравнения... это сложно, но не полностью вопрос.   -  person user541686    schedule 20.08.2012


Ответы (3)


Вы можете использовать предикаты std::less и т. д., которые гарантированно дадут общий порядок, даже если сравнение необработанных указателей этого не сделает.

Из стандарта [comparisons]/8:

Для шаблонов больше, меньше, больше_равно и меньше_равно специализации для любого типа указателя дают общий порядок, даже если встроенные операторы ‹, >, ‹=, >= этого не делают.

person Mankarse    schedule 20.08.2012
comment
Что означает total order? - person David; 20.08.2012
comment
Также связано в случае с VC++ (я подал это): -msdn" rel="nofollow noreferrer">connect.microsoft.com/VisualStudio/feedback/details/758683/ - person user541686; 20.08.2012

Но как мы это делаем? Согласно C++, если они не ссылаются на один и тот же массив, то сравнение указателей будет неопределенным!

Неправильный. Сравнение указателей не определено, а не неопределенно. Из С++ 03 §5.9/2 [expr.rel]:

[...] Указатели на объекты или функции одного и того же типа (после преобразования указателя) можно сравнивать с результатом, определенным следующим образом:

[...]
-Другие сравнения указателей не указаны.

Так что безопасно проверить, есть ли перекрытия, прежде чем делать дорогую, но правильную копию.

Интересно, что C99 отличается от C++ тем, что сравнение указателей между несвязанными объектами является неопределенным поведением. Из C99 §6.5.8/5:

При сравнении двух указателей результат зависит от относительного расположения в адресном пространстве объектов, на которые они указывают. [...] Во всех остальных случаях поведение не определено.

person Adam Rosenfield    schedule 20.08.2012
comment
Как неопределенный результат будет полезен для проверки на перекрытие? Если он не указан, то любой результат не имеет значения (просто гарантируется, что ваш жесткий диск не будет переформатирован или что-то еще). - person Mankarse; 20.08.2012
comment
Кроме того, стоит отметить, что в случае с Visual C++ в MSDN специально указано, что сравнение несвязанных указателей не гарантируется, поэтому в некоторых реализациях оно в любом случае не определено. :П - person user541686; 20.08.2012
comment
@Mankarse: неуказанный результат полезен, потому что если у вас есть массив [a, a+length] и указатель p, вы можете проверить, указывает ли p на массив с помощью if (p >= a && p < a+length). Вам все равно, какое условие ложно, главное, чтобы оно было ложным. - person Adam Rosenfield; 20.08.2012
comment
@Mehrdad: У вас есть ссылка на это? Если это так, то это просто означает, что Visual C++ не является строго соответствующей реализацией C++. Хотя я был бы очень, очень удивлен, если бы Visual C++ мог генерировать код для любой из поддерживаемых им ISA, у которых не было определенного (еще не определенного) поведения. - person Adam Rosenfield; 20.08.2012
comment
@AdamRosenfield: Вы могли бы сами поискать, но здесь: msdn.microsoft .com/en-us/library/ya5wt12d.aspx Сравнение указателей гарантируется действительным только в том случае, если указатели ссылаются на объекты в одном массиве или на местоположение, следующее за концом массива. - person user541686; 20.08.2012
comment
Эта формулировка оставляет место для интерпретаций. ...гарантировано, действительно только... не означает, что он не указан или не определен. - person Adam Rosenfield; 21.08.2012

На самом деле это было бы верно, даже если бы они были обычными итераторами. Никто ничего не мешает делать

std::vector<int> v; 
// fill v 
v.insert(v.end() - 3, v.begin(), v.end());

Определение того, являются ли они псевдонимом, является проблемой для любой реализации итераторов.

Однако вам не хватает того, что вы являетесь реализацией, вам не нужно использовать переносимый код. Как реализация, вы можете делать все, что хотите. Вы можете сказать: «Ну, в моей реализации я использую x86, а < и > можно использовать для любых указателей». И это было бы хорошо.

person Puppy    schedule 20.08.2012
comment
Ну, x86 не имеет к этому никакого отношения... кроме того факта, что мы будем игнорировать сегментацию, компилятор все равно может оптимизировать код. :-) И для моего случая (VC++) MSDN говорит, что конкретно не гарантирует правильное поведение здесь, поэтому было бы неразумно предполагать обратное, тем более что я не хочу привязывать свой код к VС++. Что касается итераторов: да, но когда вы являетесь реализацией, все сводится к использованию либо указателей, либо смещений (у которых не было бы такой же проблемы, но в целом это было бы медленнее), и указатели демонстрируют эту проблему. - person user541686; 20.08.2012