Является ли это законным использованием reinterpret_cast, и если нет, то как мне это сделать?

Этот код демонстрирует проблему, которую я пытаюсь решить:

#include <map>

class Point
{
public:
    float m_x;
    float m_y;
};

typedef std::set<Point *> PointSet;

typedef std::set<const Point * const> ConstPointSet;

float GetMinimumRange(const ConstPointSet &pointSet)
{
    float minimumRange(0.0f);
    // find the smallest distance between any pair of points in the set
    return minimumRange;
}

float GetMinimumRangeWrong(const PointSet &pointSet)
{
    PointSet::iterator first(pointSet.begin());
    Point * point(*first);
    point->m_x = 42.0f;            // I want to prevent this
    return 0.0f;
}

class PointSet_
{
public:
    std::set<Point *> m_pointSet;

    float GetMinumumRange() const
    {
        PointSet::iterator first(m_pointSet.begin());
        Point * point(*first);
        point->m_x = 42.0f;            // I want to prevent this
        return 0.0f;
    }
};

void test()
{
    PointSet myPointSet;
    // Add some points to my set

    // This fails because the compiler states it can't convert from PointSet to ConstPointSet.
    //float minimumRange1(GetMinimumRange(myPointSet));

    // reinterpret_cast<> is the only cast that works here, const_cast fails with the same
    // complaint as the line above generates
    ConstPointSet *myConstPointSet(reinterpret_cast<ConstPointSet *>(&myPointSet));

    float minimumRange1(GetMinimumRange(*myConstPointSet));

    float minimumRange2(GetMinimumRangeWrong(myPointSet));
}

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

Проблема в том, что компилятор правильно рассматривает PointSet и ConstPointSet как разные типы из-за разницы в квалификаторах const внутреннего типа и поэтому отказывается выполнять приведение между ними, хотя я добавляю только квалификаторы const.

Я попытался создать класс, содержащий PointSet, и создать функцию-член const, но даже там он позволяет модифицировать один из внутренних Point. По крайней мере, MSVC скомпилирует это без жалоб. Признаюсь, я был весьма удивлен этим.

Единственный способ, который я нашел, это использовать reinterpret_cast<> для преобразования указателя на PointSet в указатель на ConstPointSet. Стандарт отмечает, что reinterpret_cast<> можно использовать для добавления квалификаторов const, но применимо ли это в данном случае?

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

  1. Они гарантируют, что если кто-либо когда-либо изменит GetMinimumRange(), они не смогут изменить PointSet.

  2. Это позволит компилятору оптимизировать вызов GetMinimumRange(). При отсутствии квалификаторов const на вызывающем сайте нельзя делать никаких предположений относительно значений, которые могут кэшироваться во время вызова, что может привести к избыточной выборке данных.


person dgnuff    schedule 19.02.2015    source источник
comment
Вам не нужно отмечать указатель const, если контейнер является константным. Лично я бы просто передал копию контейнера и не имел указателей на элементы. Таким образом, физически невозможно изменить значение, на которое указывают указатели, и вы можете просто позволить компилятору оптимизировать.   -  person BWG    schedule 20.02.2015
comment
Это приводит к неопределенному поведению, нарушая строгое сглаживание. Псевдонимы типов — это set<T> и set<U>, которые не имеют отношения, если только T и U не идентичны.   -  person M.M    schedule 20.02.2015
comment
@BWG удаление указателей сработает, но это упрощенный пример кода для демонстрации проблемы. На практике Point * на самом деле являются указателями на Entites в игре. Использование набора‹› из них, а не набора указателей, будет иметь некоторые очевидные проблемы с производительностью.   -  person dgnuff    schedule 20.02.2015
comment
@MattMcNabb, это не решает проблему. Единственное место, где существует ConstPointSet, находится в прототипе GetMinimumRange(), и оно там по причинам, указанным в нижней части моего исходного сообщения. Если я создаю шаблон GetMinimumRange(), я мог бы также удалить ConstPointSet, что оставляет меня в ситуации, когда у меня есть подпрограмма, которая хочет принять PointSet, но также гарантирует, что она никоим образом не изменит ее. Учитывая проблему с псевдонимами, кажется, что для этого нет решения.   -  person dgnuff    schedule 20.02.2015
comment
@dgnuff Хорошо, теперь я понимаю, откуда ты идешь   -  person M.M    schedule 20.02.2015
comment
Я действительно удивлен, что PointSet::iterator first(pointSet.begin()); компилируется. Разве функция-член begin() не должна возвращать здесь const_iterator?   -  person MikeMB    schedule 20.02.2015
comment
обратите внимание, что ошибка должна появиться на Point * point(*first); , а не на point->m_x = 42;, если вы хотите, чтобы здесь была ошибка   -  person M.M    schedule 20.02.2015
comment
@MikeMB Да, компилируется. По крайней мере, это происходит с MSVC. Вот именно эту проблему я и пытаюсь здесь решить, я хочу устроить так, чтобы этого не было.   -  person dgnuff    schedule 20.02.2015
comment
@MattMcNabb Действительно. Я бы хотел, чтобы единственной допустимой формой этой строки была const Point * point(*first); Я, конечно, могу написать это, если захочу, но меня не заставляют это делать.   -  person dgnuff    schedule 20.02.2015
comment
@MikeMB Элементы в наборе в любом случае постоянны (чтобы предотвратить изменение ключей и нарушение дерева поиска), поэтому set::iterator и set::const_iterator - это одно и то же. Вот почему он компилируется.   -  person Michael Karcher    schedule 20.02.2015
comment
Меня конкретно интересовал этот LoC. У вас была бы та же проблема, если бы first был определен как const_iterator. Однако потом я узнал, что std::set::const_iterator и std::set::iterator на самом деле могут быть одного и того же типа, так как оба в любом случае указывают на константный элемент. Чтобы относиться к вашему вопросу: если количество элементов невелико, вероятно, было бы жизнеспособным решением скопировать указатели из std::set<Point *> в std::set<Point const*> перед передачей их функции   -  person MikeMB    schedule 20.02.2015
comment
@dgnuff: Кстати: вам действительно нужен набор? Или можно использовать другой контейнер?   -  person MikeMB    schedule 20.02.2015
comment
Еще одна вещь: насколько я знаю, компиляторы все равно не могут оптимизировать на основе константности, поскольку на самом деле это не гарантирует, что биты в памяти останутся неизменными (например, из-за const_cast или изменяемых элементов данных).   -  person MikeMB    schedule 20.02.2015
comment
@MikeMB Я мог бы использовать что угодно: std::vector<>, std::list<> и т. д. Учитывая, что эти наборы строятся по одному на игровой сеанс, я мог бы даже использовать простой Point **, выделенный через new[], мне не нужно делать find() на std::set<> в реальный мир, поэтому порядок не важен, мне просто нужно перебрать все члены.   -  person dgnuff    schedule 20.02.2015
comment
Оптимизация @MikeMB, которая удаляет мою причину 2. как действительную, но все еще есть причина 1: я ДОЛЖЕН НЕ хочу, чтобы std::set<> менялось.   -  person dgnuff    schedule 20.02.2015
comment
Даже помимо строгого псевдонима, [class.mfct.non-static]/p2 (если нестатическая функция-член класса X вызывается для объекта, который не относится к типу X или типу, производному от X, поведение не определено.) означает, что вы не можете законно вызывать какую-либо нестатическую функцию-член для результата вашего static_cast.   -  person T.C.    schedule 20.02.2015
comment
Тогда вы сможете использовать решение @Matt McNabb. Просто используйте std::vector с его qualifier_ptr или сделайте константные копии указателей.   -  person MikeMB    schedule 20.02.2015
comment
typedef std::set<const Point * const> не помещайте туда вторую константу.   -  person Neil Kirk    schedule 20.02.2015
comment
@BenVoigt дубликат отвечает на вопрос, безопасно ли это, но не решает вопрос об обходном пути, который задал OP   -  person M.M    schedule 26.02.2015
comment
@MattMcNabb: существует a обходной путь: используйте тип, который, как вы знаете, не является специализированным. Кроме того, часть обходного пути является дубликатом stackoverflow.com/q/9142372/103167, но я не могу закрыть как обман из двух вопросов. Таковы проблемы, которые возникают, когда вопрос действительно содержит несколько вопросов. Кроме того, это название бесполезно. Итак: вопрос, который не доступен для поиска, потому что заголовок удивительно свободен от содержания, не соответствует правилу «один вопрос на вопрос» и не добавляет ничего нового на сайт. ИМО, которое должно оставаться закрытым.   -  person Ben Voigt    schedule 26.02.2015
comment
@BenVoigt Очевидно, многие люди не согласны с вами по поводу полезности названия. Вы лично можете быть не в состоянии понять это, но, как видно из многочисленных ответов, несколько других людей прекрасно это поняли. Также stackoverflow.com/q/9142372/103167 НЕ является дубликатом этого. Этот вопрос касается константности контейнера, я имею дело с константностью элементов, на которые указывают указатели внутри контейнера. Это очень разные проблемы.   -  person dgnuff    schedule 01.03.2015
comment
@dgnuff: Твой титул бесполезен. Я не говорил, что не могу этого понять. Но это не служит цели названия, которое состоит в том, чтобы определить проблему, с которой вы столкнулись. И это дубликат другого вопроса, который я пометил как дубликат. Суть обоих заключается в том, взаимозаменяемы ли экземпляры стандартных библиотечных типов, если они различаются константной квалификацией параметра шаблона. Ответ на оба вопроса заключается в том, что нет, экземпляры с разными типами не взаимозаменяемы, даже если типы параметров совместимы друг с другом по макету.   -  person Ben Voigt    schedule 01.03.2015
comment
@BenVoigt Прочитайте stackoverflow.com/q/9142372/103167 еще раз. В особенности обратите внимание на последний вопрос Сета Карнеги и ответ Миро. Миро не хочет менять константность указываемых на объекты. Он прямо заявляет, что хочет поддерживать изменчивость Object, на которые указывает его список‹› что является полной противоположностью того, что я хочу   -  person dgnuff    schedule 01.03.2015
comment
@dgnuff: Только потому, что вы так узко определяете проблему. В обоих случаях желательно иметь представление коллекции, предоставляющее доступ с различной константной квалификацией. Добавление или удаление const является тривиальной и очевидной модификацией решения.   -  person Ben Voigt    schedule 01.03.2015
comment
@BenVoigt Это просто неправда. В общем случае взять set<MyClass *> и преобразовать его в const set<MyClass *> тривиально, и это разрешено спецификацией, просто добавьте ключевое слово const. Это то, что Миро хочет сделать. Вместо этого я хочу преобразовать set<MyClass *> в set<const MyClass *>, что явно не разрешено. Если один случай явно разрешен спецификацией, а другой явно запрещен, как они могут быть одной и той же проблемой?   -  person dgnuff    schedule 01.03.2015
comment
@dgnuff: мне жаль, что вы неправильно поняли вопрос, на который я ссылался. Миро хотел вернуть представление, которое не допускало операций с контейнерами (вставка, стирание, push_back), но допускало перезапись отдельных элементов. Как вы говорите, возврат ссылки const на контейнер тривиален, но не обеспечивает требуемого поведения.   -  person Ben Voigt    schedule 01.03.2015
comment
Давайте продолжим обсуждение в чате.   -  person dgnuff    schedule 01.03.2015


Ответы (3)


Прямого пути нет, потому что constness не распространяется через указатели. В const PointSet сами указатели являются const, а не объекты, на которые они указывают. И, как вы уже выяснили, const Point * — это тип, отличный от Point *, поэтому std::set<const Point *> — это тип, отличный от std::set<Point *>.

Мне не нравится reinterpret_cast структуры STL. Это страшно для меня. STL выполняет всевозможные оптимизации в зависимости от типа параметров шаблона. std::vector<bool> является крайним примером. Вы могли бы подумать, что std::set<T *> и std::set<const T *> будут расположены одинаково, потому что они оба являются указателями, но я бы так не считал, пока не прочитал это в Стандарте.

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

Вы можете написать класс-оболочку, который содержит ссылку на std::set<Point *>, но разрешает const доступ только к Points, на который он указывает, через итераторы. Если указатели гарантированно не null, ваш итератор может напрямую разыменовать точки. Я написал это здесь в качестве шаблона:

template <typename T>
class PointerSetViewer
{
public:
    PointerSetViewer(std::set<T *> const &set) : set(set) {}

    struct iterator : public std::iterator<std::forward_iterator_tag, T const>
    {
        iterator(typename std::set<T *>::const_iterator it) : it(it) {}
        T const &operator*() const { return **it; }
        T const *operator->() const { return *it; }
        iterator &operator++() { ++it; return *this; }
        bool operator==(iterator other) { return it == other.it; }
        bool operator!=(iterator other) { return it != other.it; }
    private:
        typename std::set<T *>::const_iterator it;
    };

    iterator begin() { return iterator(set.cbegin()); }
    iterator end() { return iterator(set.cend()); }

private:
    std::set<T *> const &set;
};

Он громоздкий, но он достигает ваших целей, не делая ничего рискованного:

float GetMinimumRangeWrong(PointerSetViewer<Point> &pointSet)
{
    PointerSetViewer<Point>::iterator first(pointSet.begin());
    first->m_x = 42.0f;            // does not compile
}

Кроме того, если вы используете С++ 11, вы можете получить несколько хороших циклов for на основе диапазона:

template <typename T>
PointerSetViewer<T> view_set(std::set<T *> const &set) {
    return PointerSetViewer<T>(set);
}

for (Point const &p : view_set(myPointSet)) {
    // whatever...
}

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

person japreiss    schedule 19.02.2015

Редактировать: это не работает для множества. Как указано в комментариях, неконстантный set определен для хранения const T, поэтому на самом деле мы ничего не можем сделать.

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


Вот решение, которое я придумал; заставьте set содержать небольшую оболочку, которая будет распространять const себя на указатель.

Я бы подумал, что уже существует класс, который делает это, но ни один из классов умных указателей std, похоже, этого не делает.

#include <iostream>
#include <set>

template<typename T>
struct qualifier_ptr
{
    T *operator->() { return ptr; }
    T const *operator->() const { return ptr; }

    operator T*() { return ptr; }
    operator T const*() const { return ptr; }

    qualifier_ptr(T *p): ptr(p) {}

private:
    T *ptr;
};

struct Point
{
    float m_x;
    float m_y;
};

struct PointSet
{
    typedef std::set< qualifier_ptr<Point> > SetType;
    SetType points;

    float foo() const
    {
        //Point *p = *points.begin();               // error
        Point const *p = *points.begin();           // OK
        return 0;
    }
};

int main()
{
    PointSet ps;
    PointSet const &cps = ps;
    ps.foo();    // OK
    cps.foo();   // OK
}

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

person M.M    schedule 19.02.2015
comment
Я сам набросал что-то подобное, чтобы ответить на этот вопрос, но отказался. Поскольку std::set<T>::value_type это const T, нет никакой разницы между std::set<qualifier_ptr<Point>> и std::set<const qualifier_ptr<Point>>. Таким образом, строка, которую вы прокомментировали с ошибкой, также не будет работать в неконстантной функции foo. - person Michael Karcher; 20.02.2015
comment
@MichaelKarcher ааа... так что мое решение работает для vector, но не для set. - person M.M; 20.02.2015
comment
Это также будет работать для значений в std::map. Поскольку OP включает заголовок <map> вместо заголовка <set>, возможно, он все еще полезен. Ваш код также должен дать сбой в <set>, потому что вы не предоставляете экземпляр operator<(qualifier_ptr<T>,qualifier_ptr<T>), std::less для qualifier_ptr<T> и не предоставляете аргумент шаблона сравнения для std::set<qualifier_ptr<T>>. Но вы можете заметить, что только когда вы на самом деле вызываете функцию в наборе, которая должна сравнивать элементы, например insert. - person Michael Karcher; 20.02.2015
comment
@MichaelKarcher да, эти вещи можно было бы легко добавить, когда он жалуется. Где-то уже должна быть такая заранее написанная оболочка, где кто-то столкнулся со всеми проблемами и исправил их. - person M.M; 20.02.2015
comment
Есть предложение для propagate_const. - person T.C.; 20.02.2015
comment
@TC выглядит очень полезной вещью, хотя, к сожалению, это все еще не помогает с проблемой std::set указателей. - person M.M; 20.02.2015

Как вы указали в комментариях, набор создается только один раз за сеанс, я бы предложил просто создать ConstPointerSet, сделав копию:

void test()
{
    PointSet myPointSet;    
    // Add some points to my set
    ConstPointSet myConstPointSet{ begin(myPointSet), end(myPointSet) };

    float minimumRange1(GetMinimumRange(myConstPointSet));
}

Или оберните его в функцию:

ConstPointSet toConst(const PointSet& pSet){
    return ConstPointSet{ cbegin(pSet), cend(pSet) };
}

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

person MikeMB    schedule 20.02.2015
comment
Хорошая мысль о std::vector‹› и std::set‹›. Вы правы, использование std::set‹›, вероятно, немного напрасно, поскольку оно будет сортировать сами указатели, что, хотя и безвредно, также является напрасной тратой усилий. Я собираюсь инкапсулировать пару std::vectors‹› во внешний класс PointSet и просто использовать то, что подходит в зависимости от варианта использования. Поскольку вы не можете перегружать константность возвращаемого типа, у меня будут просто пары методов доступа: PointSet::Foo() и PointSet::ConstFoo(). Это сделает работу. - person dgnuff; 01.03.2015