Свободный оператор → * перегружает зло?
Я читал раздел 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.
Возможно, это поведение будет четко определено. Например, проверьте, является ли он нулевым указателем и обрабатывает эту ситуацию. Так что я хочу, чтобы это было правильным решением для стандартного разрешения → * перегрузки.