Здравствуйте, LightGreen, Вы писали:
LG>Предположим, что у класса A есть контейнеры x,y, по которым константный метод calculate() вычисляет какие-то значения z. И заливает результаты в другой контейнер, переданный по ссылке. Смысл в том, что calculate() исходит из того, что класс в ходе вычислений не меняется (потому что сам метод определён как const). Но если в каком-то другом методе A::foo() разработчик решил позвать process() — и передал ему член самого класса A в качестве выходного параметра z, то получается накладка. Метод calculate начнёт писать в собственный instance и компилятор этого не заметит. Понятно, что это недостаток дизайна. Но компилятор мог бы и поругаться.
Это называется aliasing, известная и довольно сложная проблема.
Во-первых, можно обстрелять свои ноги, если по невнимательности передать два указателя на одно и то же в функцию, которая ожидает независимые значения. Простейший пример — std::copy(first, last, first + 1).
Во-вторых, даже если программист аккуратен в этом отношении, компилятор всё равно должен будет принимать предосторожности, которые вредят оптимизации. Например:
std::vector<std::ostream *> files;
populate(files);
for(size_t i = 0; i < files.size(); ++i)
{
files[i]->do_something();
}
Типичная реализация std::vector представляет собой три указателя: на первый элемент, на элемент «после последнего», и на элемент сразу после выделенной области. Т. е., size_type size() const { return last — first; } и size_type capacity() const { return end_of_storage — first; }. В вышеприведенном примере компилятор обязан вычислять size каждый раз, потому что содержащиеся в векторе указатели потенциально могут указывать на область памяти, в которой находятся значения first или last, и do_something может их перезаписывать (что, если files[0] == (std::ostream *)&files.__last?). Хотя казалось бы, что можно один раз вычислить size и потом сравнивать i с этой константой.
Или что-нибудь вроде перемножения матриц, что на C может выглядеть так: void multiply_matrices(size_t const n, int const source_left[][], int const source_right[][], int destination[][]). Оптимизатору приходится предполагать, что каждая запись в память через destination может повлиять на одну из исходных матриц, что сильно мешает ему кешировать данные.
Для решения этих проблем в C99 было добавлено правило «strict aliasing», запрещающее писать в память через указатели измененного типа, и ключевое слово restrict. Например:
void strict_aliasing_example(int* i, double* d)
{
int const i_before = *i;
*d = 0.5772156649;
int const i_after = *i;
assert(i_before == i_after);
}
void restrict_example(int* restrict i, int* restrict j)
{
int const i_before = *i;
*j = 42;
int const i_after = *i;
assert(i_before == i_after);
}
void possible_aliasing(int* i, int* j)
{
int const i_before = *i;
*j = 42;
int const i_after = *i;
printf("i was %d and it could have changed. Now i = %d.\n", i_before, i_after);
}
В первом случае оптимизатор может выкинуть второе чтение из памяти, потому что i и d разных типов и, следовательно, должны указывать на разные участки памяти (иначе UB). Во втором случае тип один и тот же, но явно указано restrict. В третьем случае приходится читать *i два раза, потому что запись по адресу j может изменять значение по адресу i.
Здравствуйте, LightGreen, Вы писали:
LG>Код без проблем компилируется и выполняется (VC++ 8.0). LG>Интересно, что это — недоработка компилятора или приоритет non-const int& и A& над const-модификатором метода?
Эта штука называется aliasing — когда к одному и тому же объекту можно доступиться под двумя разными именами/указателями/ссылками.
Модификатор доступа const не является блокировкой объекта в рантайме (мол, как только мы зашли в const-метод, все остальные тоже не смейте модифицировать этот объект под каким бы то ни было алиасом).
Если такая блокировка нужна, то требуется прибегнуть к каким-то рукоделиям.
Алиасинг нужно учитывать, и не только в данном случае.
Чаще встречается ситуация, обратная твоей
class Foo
{
char *str;
.....
void assign(Foo const& other)
{
// код, не учитывающий алиасинг, приведёт к стрельбе по памяти
free(str);
str = strdup(other.str);
}
Foo& operator=(Foo const& other) { assign(other); }
};
int main()
{
Foo a("hello");
a = a;
}
Здравствуйте, LightGreen, Вы писали:
LG>Метод calculate начнёт писать в собственный instance и компилятор этого не заметит. Понятно, что это недостаток дизайна. Но компилятор мог бы и поругаться.
. Это вопрос к программисту...
Другое дело оптимизация...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Alexander G, Вы писали:
AG>Здравствуйте, LightGreen, Вы писали:
AG>Смысл в том, что calculate() исходит из того, что класс в ходе вычислений не меняется
Одно маленьнкое уточнение, не меняется не КЛАСС, а об'ект данного класса.
Я тут собираю хорошие сообщения, чтобы их занести в новосозданную вики на RSDN. Даешь ли ты торжественное официальное согласие перенести вышеупомянутое сообщение туда на условиях неведомо какой
class A
{
int a;
bool state;
void set(int& x) const
{
x = 1;
}
void modify( A& a ) const
{
a.x++;
}
void hack()
{
set(a);
modify(*this);
}
};
Код без проблем компилируется и выполняется (VC++ 8.0).
Интересно, что это — недоработка компилятора или приоритет non-const int& и A&
над const-модификатором метода?
LG>Код без проблем компилируется и выполняется (VC++ 8.0). LG>Интересно, что это — недоработка компилятора или приоритет non-const int& и A& LG>над const-модификатором метода?
Я не понял, кто здесь x? У меня дает error C2039: 'x' : is not a member of 'A' (VS 2008).
А в чем проблема-то? То, что в const-метод передается volatile параметр, которыи можно менять? Это, конечно, можно отловить, но надо ли? Для отловки надо знать типизацию параметра — а если передается обьект наследованого типа, где изменения разрешены?
Здравствуйте, FoolS.Top, Вы писали:
FT>Здравствуйте, LightGreen, Вы писали:
LG>>Пример упрощённый, взятый из реального проекта. LG>>
LG>>Код без проблем компилируется и выполняется (VC++ 8.0). LG>>Интересно, что это — недоработка компилятора или приоритет non-const int& и A& LG>>над const-модификатором метода?
FT>Я не понял, кто здесь x? У меня дает error C2039: 'x' : is not a member of 'A' (VS 2008).
AG> int x;
AG> void modify( A& a ) const
AG> {
AG> a.x++;
AG> }
AG>
AG>- этот const относится к this только
Согласен. Другое дело — что понимать под this. В вызывающем методе тот же самый this
и в моём примере это даже на этапе компиляции известно. Собственно, вопрос был в том,
является ли это ошибкой компилятора. Уже понял, что не является.
Спасибо за развёрнутый ответ.
Ещё несколько мыслей по этой теме.
Предположим, что у класса A есть контейнеры x,y, по которым константный метод calculate() вычисляет какие-то значения z. И заливает результаты в другой контейнер, переданный по ссылке. Смысл в том, что calculate() исходит из того, что класс в ходе вычислений не меняется (потому что сам метод определён как const). Но если в каком-то другом методе A::foo() разработчик решил позвать process() — и передал ему член самого класса A в качестве выходного параметра z, то получается накладка. Метод calculate начнёт писать в собственный instance и компилятор этого не заметит. Понятно, что это недостаток дизайна. Но компилятор мог бы и поругаться.
Здравствуйте, LightGreen, Вы писали:
LG>Ещё несколько мыслей по этой теме. LG>Предположим, что у класса A есть контейнеры x,y, по которым константный метод calculate() вычисляет какие-то значения z. И заливает результаты в другой контейнер, переданный по ссылке. Смысл в том, что calculate() исходит из того, что класс в ходе вычислений не меняется (потому что сам метод определён как const). Но если в каком-то другом методе A::foo() разработчик решил позвать process() — и передал ему член самого класса A в качестве выходного параметра z, то получается накладка. Метод calculate начнёт писать в собственный instance и компилятор этого не заметит. Понятно, что это недостаток дизайна. Но компилятор мог бы и поругаться.
Очень хороший пример. Я думаю, что компилятор не может так "глубоко копать". Во всех случаях const означает, что непосредственно сам об'ект не может изменяться. "Я так думаю".
Смысл в том, что calculate() исходит из того, что класс в ходе вычислений не меняется (потому что сам метод определён как const). Но если в каком-то другом методе A::foo() разработчик решил позвать process() — и передал ему член самого класса A в качестве выходного параметра z, то получается накладка. Метод calculate начнёт писать в собственный instance и компилятор этого не заметит.
Допустим struct A { calculate(A&) const; }
В случае
A a;
a.calculate(a);
нет константности a.
В случае
A a;
A b;
a.calculate(b);
и если изменения b и глобальные вызовы не меняют a, изменить a можно будет:
1. Изменив член через mutable
2. Изменив член через const_cast или С-style cast
3. Вызвав не-костантный метод через const_cast или С-style cast
1 осознанное ограничение const корректности, 2 и 3 осознанное нарушение const корректности.
Однако, константность не глубокая, поэтому нет защиты от подобного:
struct A
{
int i;
A * that;
A() { i = 0; that = this; }
void calculate(void) const { ++that->i; }
};
LG>Код без проблем компилируется и выполняется (VC++ 8.0). LG>Интересно, что это — недоработка компилятора или приоритет non-const int& и A& LG>над const-модификатором метода?
А что здесь некорректного? В теле метода modify присутствуют две ссылки — одна константная (*this), вторая неконстантная (a). Модификация объекта происходит именно через неконстантную ссылку, а в том, что обе эти ссылки ссылаются на один объект, никакого нарушения константности нет. То же можно сказать о методе set.
Наверное, приведенный пример может сбивать с толку, если забыть о том, что у всех нестатических функций-членов неявно присутствует дополнительный параметр — указатель на объект, для которого вызывается функция-член. Модификатор const, который указывается в объявлении функции члена относится именно к этому неявному параметру и ни к какому другому. Если модифицировать код так, чтоб передача указателя на объект стала явной, то иллюзия нарушения константности исчезает (как мне кажется):
struct A
{
int a;
bool state;
};
void set(const A* this_, int& x)
{
x = 1; //модификация по неконстантной ссылке
this_->a = 1; //error
}
void modify(const A* this_, A& a)
{
a.a++; //модификация по неконстантной ссылке
this_->a++; //error
}
void i_nikakoi_ne_hack(A* this_)
{
//В обоих нижеследующих вызовах по неконстантному указателю
//образуются константный указатель и неконстнатная ссылка.
//Все законно.
set(this_, this_->a);
modify(this_, *this_);
}
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, Roman Odaisky, Вы писали:
RO>Для решения этих проблем в C99 было добавлено правило «strict aliasing», запрещающее писать в память через указатели измененного типа, и ключевое слово restrict. Например: RO>
RO>void strict_aliasing_example(int* i, double* d)
RO>{
RO> int const i_before = *i;
RO> *d = 0.5772156649;
RO> int const i_after = *i;
RO> assert(i_before == i_after);
RO>}
RO>void restrict_example(int* restrict i, int* restrict j)
RO>{
RO> int const i_before = *i;
RO> *j = 42;
RO> int const i_after = *i;
RO> assert(i_before == i_after);
RO>}
RO>void possible_aliasing(int* i, int* j)
RO>{
RO> int const i_before = *i;
RO> *j = 42;
RO> int const i_after = *i;
RO> printf("i was %d and it could have changed. Now i = %d.\n", i_before, i_after);
RO>}
RO>
RO>В первом случае оптимизатор может выкинуть второе чтение из памяти, потому что i и d разных типов и, следовательно, должны указывать на разные участки памяти (иначе UB). Во втором случае тип один и тот же, но явно указано restrict. В третьем случае приходится читать *i два раза, потому что запись по адресу j может изменять значение по адресу i.
Я бы добавил (поправь если ошибаюсь), что примеры strict_aliasing_example и
possible_aliasing относятся также к С++, а restrict_example — не относится к С++. И ещё такой случай:
void possible_aliasing_with_different_types(int* i, char unsigned* j)
{
int const i_before = *i;
*j = 42;
int const i_after = *i;
printf("i was %d and it could have changed. Now i = %d.\n", i_before, i_after);
}
RO>P. S. Надо бы раскрыть эту тему в вики.
Вики таки работает ?
.
RO>Я тут собираю хорошие сообщения, чтобы их занести в новосозданную вики на RSDN. Даешь ли ты торжественное официальное согласие перенести вышеупомянутое сообщение туда на условиях неведомо какой
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
RO>>Я тут собираю хорошие сообщения, чтобы их занести в новосозданную вики на RSDN. Даешь ли ты торжественное официальное согласие перенести вышеупомянутое сообщение туда на условиях неведомо какой