Свободный оператор → * перегружает зло?

Я читал раздел 13.5 после опровержения понятия, что встроенные операторы не участвуют в разрешении перегрузки, и заметили, что в разделе operator->* нет раздела. Это всего лишь общий двоичный оператор.

Его братья, operator->, operator* и operator[], все должны быть нестатическими функциями-членами. Это исключает определение перегрузки свободной функции оператору, обычно используемому для получения ссылки от объекта. Но необычный operator->* не учитывается.

В частности, operator[] имеет много общего. Он двоичный (они пропустили прекрасную возможность сделать его n-ary), и он принимает какой-то контейнер слева и какой-то локатор справа. Его раздел специальных правил, 13.5.5, похоже, не имеет никакого фактического эффекта, кроме как запретить бесплатные функции. (И это ограничение даже исключает поддержку коммутативности!)

Так, например, это совершенно законно:

#include <utility>
#include <iostream>
using namespace std;

template< class T >
T &
operator->*( pair<T,T> &l, bool r )
    { return r? l.second : l.first; }

template< class T >
 T & operator->*( bool l, pair<T,T> &r ) { return r->*l; }

int main() {
        pair<int, int> y( 5, 6 );
        y->*(0) = 7;
        y->*0->*y = 8; // evaluates to 7->*y = y.second
        cerr << y.first << " " << y.second << endl;
}

Легко найти использование, но альтернативный синтаксис не так уж плох. Например, масштабированные индексы для vector:

v->*matrix_width[2][5] = x; // ->* not hopelessly out of place

my_indexer<2> m( v, dim ); // my_indexer being the type of (v->*width)
m[2][5] = x; // it is probably more practical to slice just once

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

Ответ 1

Поймав немного, я нашел больше случаев, когда люди спрашивали, используется ли operator->*, чем фактические предложения.

Пара мест предлагает T &A::operator->*( T B::* ). Не уверен, отражает ли это намерение разработчика или заблуждение, что T &A::operator->*( T A::* ) является встроенным. Не совсем связано с моим вопросом, но дает представление о глубине, которую я нашел в онлайн-дискуссиях и литературе.

Было упоминание о "D & E 11.5.4", которое, я полагаю, является "Дизайн и эволюция С++". Возможно, это содержит подсказку. В противном случае, я просто закончу это немного бесполезным уродством, которое было упущено стандартизацией, и большинство остальных тоже.

Изменить Ниже приведена вставка цитаты D & E.

Чтобы выразить это количественно, ->* является самым узким оператором связывания, который может быть перегружен свободной функцией. Все перегрузки постфиксных и унарных операторов требуют нестатических сигнатур-функций-членов. Следующим приоритетом после унарных операторов являются приведения в стиле C, которые, как можно сказать, соответствуют функциям преобразования (operator type()), которые также не могут быть свободными функциями. Затем приходит ->*, а затем умножение. ->* может быть как [] или как %, они могли пойти в любом случае, и они выбрали путь EEEEEEVIL.

Ответ 2

Лучшим примером, о котором я знаю, является Boost.Phoenix, который перегружает этот оператор для реализации ленивого доступа членов.

Для тех, кто незнаком с Phoenix, это превосходная библиотека для создания актеров (или функциональных объектов), которые выглядят как нормальные выражения:

( arg1 % 2 == 1 )     // this expression evaluates to an actor
                 (3); // returns true since 3 % 2 == 1

// these actors can also be passed to standard algorithms:
std::find_if(c.begin(), c.end(), arg1 % 2 == 1);
// returns iterator to the first odd element of c

Достигает вышеуказанного, перегружая operator% и operator==. - применяется к актеру arg1, эти операторы возвращают другого актера. Диапазон выражений, которые могут быть построены таким образом, является экстремальным:

// print each element in c, noting its value relative to 5:
std::for_each(c.begin(), c.end(),
  if_(arg1 > 5)
  [
    cout << arg1 << " > 5\n"
  ]
  .else_
  [
    if_(arg1 == 5)
    [
      cout << arg1 << " == 5\n"
    ]
    .else_
    [
      cout << arg1 << " < 5\n"
    ]
  ]
);

После того, как вы некоторое время используете Phoenix (не то, чтобы вы когда-либо возвращались), вы попробуете что-то вроде этого:

typedef std::vector<MyObj> container;
container c;
//...
container::iterator inv = std::find_if(c.begin(), c.end(), arg1.ValidStateBit);
std::cout << "A MyObj was invalid: " << inv->Id() << std::endl;

Это не удастся, потому что у актеров Phoenix нет участника ValidStateBit. Феникс обходит это, перегружая operator->*:

(arg1 ->* &MyObj::ValidStateBit)              // evaluates to an actor
                                (validMyObj); // returns true 

// used in your algorithm:
container::iterator inv = std::find_if(c.begin(), c.end(), 
      (arg1 ->* &MyObj::ValidStateBit)    );

operator->* аргументы:

  • LHS: актер, возвращающий MyObj *
  • RHS: адрес участника

Он возвращает актера, который оценивает LHS и ищет в нем указанный член. (Примечание: вы действительно хотите убедиться, что arg1 возвращает MyObj * - вы не видели массивную ошибку шаблона, пока не получите что-то не так в Фениксе. Эта небольшая программа вызвала 76,738 символов боли (Boost 1.54, gcc 4.6 ):

#include <boost/phoenix.hpp>
using boost::phoenix::placeholders::arg1;

struct C { int m; };
struct D { int n; };

int main() {
  ( arg1  ->*  &D::n ) (new C);
  return 0;
}

Ответ 3

Я согласен с вами в том, что в стандарте есть некогерентность, он не позволяет перегружать operator[] не-членными функциями и позволяет использовать его для operator->*. Для моей точки зрения operator[] относится к массивам, поскольку operator->* относится к structs/classes (getter). Члены массива выбираются с использованием индекса. Члены структуры выбираются с помощью указателей-членов.

Худшим является то, что у нас может возникнуть соблазн использовать ->* вместо operator[] для получения массива, подобного элементу

int& operator->*(Array& lhs, int i);

Array a;

a ->* 2 = 10;

Существует и другая возможная некогерентность. Мы можем использовать не-членную функцию для перегрузки operator+= и всего оператора формы @=), и мы не можем сделать это для operator=.

Я действительно не знаю, в чем причина, чтобы сделать следующие юридические

struct X {
    int val;
    explicit X(int i) : val(i) {}
};
struct Z {
    int val;
    explicit Z(int i) : val(i) {}
};
Z& operator+=(Z& lhs, const X& rhs) {
    lhs.val+=rhs.val;
    return lhs;
}

Z z(2);
X x(3);
z += x;

и запретить

Z& operator=(Z& lhs, const X& rhs) {
    lhs.val=i;
    return lhs;
}

z = x;

Извините, что не ответил на ваш вопрос, но добавил еще больше путаницы.

Ответ 4

Стандарт (рабочий проект 2010-02-16, § 5.5) гласит:

Результат выражения → * является lvalue, только если его второй операнд является указатель на элемент данных. Если второй операнд - это нулевой указатель на член (4.11), поведение undefined.

Возможно, это поведение будет четко определено. Например, проверьте, является ли он нулевым указателем и обрабатывает эту ситуацию. Так что я хочу, чтобы это было правильным решением для стандартного разрешения → * перегрузки.