Могут ли несколько прокси-классов составить битовый вектор, защищенный от STL?

хорошо известно, что std::vector<bool> не удовлетворяет требованиям стандарта к контейнерам, главным образом потому, что упакованное представление не позволяет T* x = &v[i] от возврата указателя на логическое значение.

У меня такой вопрос: можно ли это исправить/смягчить, когда reference_proxy перегружает адрес operator& для возврата pointer_proxy?

Указатель-прокси может содержать те же данные, что и reference_proxy в большинстве реализаций, а именно указатель на упакованные данные и маску для изоляции определенного бита внутри блока, на который указывает. Косвенное обращение к pointer_proxy даст в результате reference_proxy. По сути, оба прокси представляют собой «толстые» указатели, которые, тем не менее, все еще довольно легкие по сравнению с контейнерами прокси на диске.

Тогда вместо T* x = &v[0] можно было бы сделать auto x = &v[0] и без проблем использовать x как if(*x). Я также хотел бы иметь возможность писать for(auto b: v) { /* ... */ }

Вопросы: будет ли такой мультипрокси-подход работать с алгоритмами STL? Или некоторые алгоритмы действительно полагаются на требование, чтобы x было реальным bool*? Или требуется слишком много последовательных пользовательских преобразований, которые мешают этому работать? Я хотел бы узнать о любом из таких препятствий, прежде чем пытаться полностью завершить приведенный выше набросок реализации.


ОБНОВЛЕНИЕ (на основе ответа @HowardHinnant и этого древнее обсуждение comp.std.c++)

Вы можете пройти долгий путь, чтобы практически имитировать встроенные типы: для любого заданного типа T пара прокси (например, reference_proxy и iterator_proxy) может быть взаимно согласована в том смысле, что reference_proxy::operator&() и iterator_proxy::operator* () являются обратными друг другу.

Однако в какой-то момент нужно отобразить обратно прокси-объекты, чтобы они вели себя как T* или T&. Для прокси-итераторов можно перегрузить оператор->() и получить доступ к интерфейсу шаблона T без повторной реализации всей функциональности. Однако для эталонных прокси вам потребуется перегрузить оператор.(), а это не разрешено в текущем C++ (хотя Себастьян Редл представил такое предложение на BoostCon 2013). Вы можете сделать подробный обходной путь, например член .get() внутри ссылочного прокси, или реализовать весь интерфейс T внутри ссылки (это то, что делается для vector::bit_reference), но это либо потеряет встроенный синтаксис или введите пользовательские преобразования, которые не имеют встроенной семантики для преобразования типов (у вас может быть не более одного пользовательского преобразования для каждого аргумента).


person TemplateRex    schedule 27.12.2012    source источник
comment
C++11 требует, чтобы reference_type было lvalue of T, поэтому требований к контейнеру недостаточно.   -  person K-ballo    schedule 30.12.2012
comment
@K-ballo Спасибо, я знаю, что это часть требований. Но где и как именно прокси-ссылки + прокси-указатели ломаются?   -  person TemplateRex    schedule 30.12.2012


Ответы (2)


Мой вопрос: можно ли это исправить/смягчить, когда reference_proxy перегружает оператор адреса и возвращает pointer_proxy?

libc++ действительно делает это.

#include <vector>
#include <cassert>

int main()
{
    std::vector<bool> v(1);
    std::vector<bool>::pointer pb = &v[0];
    assert(*pb == false);
    *pb = true;
    assert(v[0] == true);
    std::vector<bool>::const_pointer cbp = pb;
    assert(*cbp == true);
    v[0] = false;
    assert(*cbp == false);
}

Он даже распространяется на const_pointer и const_reference таким образом, что имитирует те же типы для vector<int>. Это несоответствующее расширение для libc++. Но это значительно повышает вероятность того, что написание универсального кода, который может быть создан на vector<bool>, скомпилируется и будет вести себя правильно.

Вопросы: будет ли такой мультипрокси-подход работать с алгоритмами STL? Или некоторые алгоритмы действительно полагаются на требование, чтобы x был настоящим логическим значением*? Или требуется слишком много последовательных пользовательских преобразований, которые мешают этому работать?

Все алгоритмы libc++ работают с vector<bool>. Некоторые из них демонстрируют весьма впечатляющую производительность. В частности, один алгоритм должен иметь специальную обработку, которая, к сожалению, не предусмотрена стандартом:

#include <vector>
#include <cassert>

int main()
{
    std::vector<bool> v(1);
    bool b = true;
    assert(v[0] == false);
    assert(b == true);
    std::swap(b, v[0]);
    assert(v[0] == true);
    assert(b == false);
}

Это очень легко реализовать. Просто нужно убедиться, что swap работает для любой комбинации bool и vector<bool>::reference. Но я не знаю, делает ли это какая-либо реализация, кроме libС++, и это не предписано С++ 11.

Массив битов — это замечательная структура данных. Но, к сожалению, это плохо указано в стандарте C++. libc++ объявлен вне закона, чтобы продемонстрировать, что это может быть очень полезная и высокопроизводительная структура данных. Есть надежда, что будущий стандарт C++ сможет двигаться в этом направлении, что принесет пользу программистам на C++.

person Howard Hinnant    schedule 26.02.2013
comment
+1 и принято! Хотел бы я найти заголовок bit_reference в libc++ SVN сайт раньше... Еще больше стимулов для начала компиляции последней версии Clang/libc++ в Linux (где те дистрибутивы, которые предоставляют пакеты для Clang ›= 3.1??) - person TemplateRex; 26.02.2013
comment
Интересно, имеет ли смысл определить шаблон proxy_traits<T> (похожий на allocator_traits<T>), который контейнеры STL могли бы использовать для выбора стратегии их реализации (дисковое, упакованное представление и т. д.)? - person TemplateRex; 26.02.2013

Навскидку я бы сказал, во-первых, что на самом деле это будет больше зависеть от особенностей каждой отдельной реализации STL, поскольку она официально не соответствует стандартному требованию, согласно которому *reference_type должен быть lvalue T*. Итак, что касается потенциальных проблем с реализацией:

Основная причина, по которой любой фрагмент кода будет явно зависеть от указателя контейнера, являющегося настоящим bool*, заключается в том, что алгоритм использует арифметику указателя, и в этом случае размер типа указателя становится важным. Однако арифметика указателей обошла бы интерфейс итератора и, таким образом, нарушила бы основную цель всей конструкции STL «контейнер-за-итератором». Сам std::vector‹> гарантированно будет непрерывным в C++11, что позволяет оптимизировать специализацию как алгоритмов STL, так и компилятора for(:), оба из которых могут использовать внутреннюю арифметику указателей. Если ваш тип не является производным от std::vector, это не должно быть проблемой; вместо этого все должно просто предполагать метод итератора.

Однако! код STL по-прежнему может принимать указатели не для арифметических операций с указателями, а для какой-то другой цели. В данном случае проблема заключается в синтаксисе C++. Например, цитируя свой вопрос:

Вместо T* x = &v[0] можно было сделать auto x = &v[0]

Любой шаблонный код в STL также должен будет делать то же самое... и на данный момент кажется совершенно маловероятным, что реализации STL будут широко использовать auto. Могут быть и другие ситуации, когда STL пытается использовать хитрые приемы приведения r-значений, которые в конечном итоге терпят неудачу, потому что не ожидают несовпадающих ссылочных типов.

Относительно for(auto b: v) { /* ... */ }: не вижу причин, по которым это не должно работать. Я думаю, что он будет генерировать код, который будет гораздо менее эффективным, чем та же версия, которую вы могли бы просто свернуть самостоятельно за 15 минут (или меньше). Я упоминаю об этом только потому, что вы упомянули внутренние элементы в OP, что требует некоторого внимания к производительности. Вы также не сможете помочь с помощью встроенных функций. Встроенная функция не может сделать ничего лучше простого побитового сдвига для последовательного обхода массива битов. Большая часть дополнительных накладных расходов будет связана с созданием компилятором кода для обновления указателя итератора и значений маски, а затем перезагрузки значения маски на следующей итерации. Он не сможет волшебным образом вывести то, что вы пытаетесь сделать, и превратить это в последовательную операцию смены для вас. По крайней мере, он может оптимизировать этап обновления указателя + обратная запись, кэшируя его в регистр вне цикла, хотя, честно говоря, я бы очень скептически отнесся к этому, основываясь на своем опыте.

Вот один из способов прохождения битов от начала до конца, просто для сравнения (версия, способная начинаться в любой произвольной точке битового потока, потребует немного дополнительной логики настройки):

uint64_t* pBitSet   = &v[-1];   // gets incremented on first iteration through loop.
uint64_t  curBitSet =  v[0];

for (int i=0; i<v.length(); ++i)  {
    if ((i % 64) == 0) {
       curBitSet = *(++pBitSet);
    }
    int bit = curBitSet & 1;
    curBitSet >>= 1;

    // do stuff based on 'bit' here.
}
person jstine    schedule 31.12.2012
comment
Если я определяю typdef pointer_proxy pointer; и typedef reference_proxy reference;, не будет ли алгоритм STL использовать pointer x = &v[0];? В противном случае какой смысл иметь эти вложенные определения типов внутри контейнеров STL? И чтобы получить правильную арифметику указателя, я всегда мог бы перегрузить operator+ для pointer_proxy, верно? - person TemplateRex; 01.01.2013
comment
Что касается ручного проката материала за 15 минут: я знаю, как выполнять итерацию с помощью битового вращения. Но такой битовый вектор будет деталью реализации и должен соответствовать более общим интерфейсам STL. Говард Хиннант написал недавний блог isocpp.org/blog/2012/11/on-vectorbool о том, как перегрузить многие алгоритмы STL для итераторов битовых векторов. Но поскольку я не являюсь автором библиотеки STL, мне интересно, смогу ли я получить некоторую отдачу от использования прокси-классов. - person TemplateRex; 01.01.2013
comment
Если реализация STL придерживается использования typedefs, то теоретически да, это должно работать. Поскольку стандарт явно не требует этого (например, делая заявление о том, что тип может быть известен), реализация не будет неправильной, чтобы обойти тип указателя. Так что простого ответа относительно контейнера, соответствующего стандарту, по-прежнему нет. Это можно рассматривать как дефект стандарта (их много). Часто реализации STL придерживаются более высокого стандарта, чем даже сам стандарт, и эти более высокие стандарты иногда становятся официальным стандартом. - person jstine; 01.01.2013
comment
Об арифметике указателей: перегрузка operator+ и всего остального, кроме операций mult и div, в значительной степени. Я думаю, что это должно сработать, пока ничто не пытается преобразовать указатель в целое число, использовать арифметику sizeof (T) и затем снова. (что, я уверен, никогда не происходит ни в одной реализации STL). - person jstine; 01.01.2013