В программе, которую я пишу, возник следующий шаблон. Я надеюсь, что он не слишком надуман, но ему удается мутировать объект Foo
в методе const Foo :: Questionable () const
, без использования какого-либо const_cast или подобного. В принципе, Foo
хранит ссылку на FooOwner
и наоборот, а в Questionable ()
, Foo
удается изменить себя в методе const путем вызов mutate_foo ()
для его владельца. Вопросы следуют за кодом.
#include "stdafx.h"
#include <iostream>
using namespace std;
class FooOwner;
class Foo {
FooOwner& owner;
int data;
public:
Foo(FooOwner& owner_, int data_)
: owner(owner_),
data(data_)
{
}
void SetData(int data_)
{
data = data_;
}
int Questionable() const; // defined after FooOwner
};
class FooOwner {
Foo* pFoo;
public:
FooOwner()
: pFoo(NULL)
{}
void own(Foo& foo)
{
pFoo = &foo;
}
void mutate_foo()
{
if (pFoo != NULL)
pFoo->SetData(0);
}
};
int Foo::Questionable() const
{
owner.mutate_foo(); // point of interest
return data;
}
int main()
{
FooOwner foo_owner;
Foo foo(foo_owner, 0); // foo keeps reference to foo_owner
foo_owner.own(foo); // foo_owner keeps pointer to foo
cout << foo.Questionable() << endl; // correct?
return 0;
}
Это определенное поведение? Должен ли Foo :: data
быть объявлен изменяемым? Или это признак того, что я делаю что-то неправильно? Я пытаюсь реализовать что-то вроде «ленивых» инициализированных «данных», которые устанавливаются только по запросу, и следующий код прекрасно компилируется без предупреждений, поэтому я немного нервничаю, находясь в стране UB.
Редактировать : const
для Questionable () создает только непосредственные члены const, а не объекты, на которые указывает или на которые ссылается объект. Делает ли это код легальным? Я' Я сбит с толку тем фактом, что в Questionable ()
, этот
имеет тип const Foo *
и далее вниз по стеку вызовов, FooOwner
легитимно имеет неконстантный указатель, который он использует для изменения Foo
. Означает ли это, что объект Foo
можно изменить или нет?
Редактировать 2: возможно, еще более простой пример:
class X {
X* nonconst_this; // Only turns in to X* const in a const method!
int data;
public:
X()
: nonconst_this(this),
data(0)
{
}
int GetData() const
{
nonconst_this->data = 5; // legal??
return data;
}
};
Рассмотрим следующее:
int i = 3;
i
- это объект, имеющий тип int
. Это не cv-квалифицированный (не const
или volatile
, или оба).
Теперь мы добавляем:
const int& j = i;
const int* k = &i;
j
- это ссылка, которая относится к i
, а k
- указатель, который указывает на i
. (С этого момента мы просто объединяем «ссылаться на» и «указывает на», чтобы просто «указать на».)
На данный момент у нас есть две переменные с квалификацией cv, j
и ] k
, которые указывают на объект, не квалифицированный cv. Это упоминается в § 7.1 / 3:
Указатель или ссылка на cv-квалифицированный тип не обязательно должны указывать или ссылаться на cv-квалифицированный объект, но он обрабатывается так, как если бы он это делал; константный путь доступа не может использоваться для изменения объекта, даже если указанный объект не является константным объектом и может быть изменен через какой-либо другой путь доступа. [Примечание: cv-квалификаторы поддерживаются системой типов, поэтому их нельзя преобразовать без преобразования (5.2.11). ]
Это означает, что компилятор должен учитывать, что j
и k
квалифицированы cv, даже если они указывают на объект, не квалифицированный cv. (Итак, j = 5
и * k = 5
недопустимы, хотя i = 5
допустимо.)
Теперь мы рассмотрим удаление ] const
из тех:
const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;
Это допустимо (§см. 5.2.11), но является ли это неопределенным поведением? Нет. См. §7.1 5.1 / 4:
За исключением того, что любой член класса, объявленный изменяемым (7.1.1), может быть изменен, любая попытка изменить константный объект во время его жизни (3.8) приводит к неопределенному поведению . Акцент мой.
Помните, что i
равно не const
, и что j
и k
оба указывают на i
. Все, что мы сделали, это сказали системе типов удалить квалификатор const из типа, чтобы мы могли изменить объект, на который указывает объект, а затем изменили i
с помощью этих переменных.
Это в точности то же самое, что и действие:
int& j = i; // removed const with const_cast...
int* k = &i; // ..trivially legal code
j = 5;
*k = 5;
И это тривиально законно. Теперь мы считаем, что i
было вместо этого:
const int i = 3;
Что теперь с нашим кодом?
const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;
Теперь он приводит к неопределенному поведению , потому что i
является Константный объект. Мы сказали системе типов удалить const
, чтобы мы могли изменить объект, на который указывает объект, , а затем изменили объект с квалификацией const . Это не определено, как указано выше.
Опять же, более очевидно, как:
int& j = i; // removed const with const_cast...
int* k = &i; // ...but this is not legal!
j = 5;
*k = 5;
Обратите внимание, что простое выполнение этого:
const_cast<int&>(j);
*const_cast<int*>(k);
Совершенно законно и определено, поскольку никакие объекты с квалификацией const не изменяются; мы просто возимся с системой типов.
Теперь подумайте:
struct foo
{
foo() :
me(this), self(*this), i(3)
{}
void bar() const
{
me->i = 5;
self.i = 5;
}
foo* me;
foo& self;
int i;
};
Что const
на баре
делает с членами? Это делает доступ к ним через так называемый cv-квалифицированный путь доступа . (Это достигается путем изменения типа this
с T * const
на cv T const *
, где cv
- это cv- квалификаторы функции.)
Итак, каковы типы элементов во время выполнения bar
? Это:
// const-pointer-to-non-const, where the pointer points cannot be changed
foo* const me;
// foo& const is ill-formed, cv-qualifiers do nothing to reference types
foo& self;
// same as const int
int const i;
Конечно, типы не имеют значения, поскольку важна константная квалификация , указывающая на объекты, а не указатели. (Если бы k
выше было const int * const
, последнее const
не имеет значения.) Теперь рассмотрим:
int main()
{
foo f;
f.bar(); // UB?
}
В пределах бара
, оба me
и self
указывают на неконстантный foo
, поэтому, как и в случае с int i
выше, мы имеем четко определенное поведение . Если бы у нас было:
const foo f;
f.bar(); // UB!
У нас был бы UB, как и в случае с const int
, потому что мы бы изменяли объект с квалификацией const.
В вашем вопросе у вас нет объектов с квалификацией const, поэтому у вас нет неопределенного поведения.
И просто чтобы добавить апелляцию к авторитету, рассмотрим уловку const_cast
Скотта Мейерса, использованную для преобразования константной функции в неконстантную функцию:
struct foo
{
const int& bar() const
{
int* result = /* complicated process to get the resulting int */
return *result;
}
int& bar()
{
// we wouldn't like to copy-paste a complicated process, what can we do?
}
};
Он предлагает:
int& bar(void)
{
const foo& self = *this; // add const
const int& result = self.bar(); // call const version
return const_cast<int&>(result); // take off const
}
Или как это обычно пишут:
int& bar(void)
{
return const_cast<int&>( // (3) remove const from result
static_cast<const foo&>(*this) // (1) add const to this
.bar() // (2) call const version
);
}
Обратите внимание, что это опять же совершенно законно и четко определено. В частности, поскольку эта функция должна вызываться для неконстантного foo
, мы совершенно безопасно убираем константную квалификацию из типа возвращаемого значения int & boo () const
.
(Если только кто-то не выстрелит в себя вызовом const_cast
+ в первую очередь.)
Подведем итог:
struct foo
{
foo(void) :
i(),
self(*this), me(this),
self_2(*this), me_2(this)
{}
const int& bar() const
{
return i; // always well-formed, always defined
}
int& bar() const
{
// always well-formed, always well-defined
return const_cast<int&>(
static_cast<const foo&>(*this).
bar()
);
}
void baz() const
{
// always ill-formed, i is a const int in baz
i = 5;
// always ill-formed, me is a foo* const in baz
me = 0;
// always ill-formed, me_2 is a const foo* const in baz
me_2 = 0;
// always well-formed, defined if the foo pointed to is non-const
self.i = 5;
me->i = 5;
// always ill-formed, type points to a const (though the object it
// points to may or may not necessarily be const-qualified)
self_2.i = 5;
me_2->i = 5;
// always well-formed, always defined, nothing being modified
// (note: if the result/member was not an int and was a user-defined
// type, if it had its copy-constructor and/or operator= parameter
// as T& instead of const T&, like auto_ptr for example, this would
// be defined if the foo self_2/me_2 points to was non-const
int r = const_cast<foo&>(self_2).i;
r = const_cast<foo* const>(me_2)->i;
// always well-formed, always defined, nothing being modified.
// (same idea behind the non-const bar, only const qualifications
// are being changed, not any objects.)
const_cast<foo&>(self_2);
const_cast<foo* const>(me_2);
// always well-formed, defined if the foo pointed to is non-const
// (note, equivalent to using self and me)
const_cast<foo&>(self_2).i = 5;
const_cast<foo* const>(me_2)->i = 5;
// always well-formed, defined if the foo pointed to is non-const
const_cast<foo&>(*this).i = 5;
const_cast<foo* const>(this)->i = 5;
}
int i;
foo& self;
foo* me;
const foo& self_2;
const foo* me_2;
};
int main()
{
int i = 0;
{
// always well-formed, always defined
int& x = i;
int* y = &i;
const int& z = i;
const int* w = &i;
// always well-formed, always defined
// (note, same as using x and y)
const_cast<int&>(z) = 5;
const_cast<int*>(w) = 5;
}
const int j = 0;
{
// never well-formed, strips cv-qualifications without a cast
int& x = j;
int* y = &j;
// always well-formed, always defined
const int& z = i;
const int* w = &i;
// always well-formed, never defined
// (note, same as using x and y, but those were ill-formed)
const_cast<int&>(z) = 5;
const_cast<int*>(w) = 5;
}
foo x;
x.bar(); // calls non-const, well-formed, always defined
x.bar() = 5; // calls non-const, which calls const, removes const from
// result, and modifies which is defined because the object
// pointed to by the returned reference is non-const,
// because x is non-const.
x.baz(); // well-formed, always defined
const foo y;
y.bar(); // calls const, well-formed, always defined
const_cast<foo&>(y).bar(); // calls non-const, well-formed,
// always defined (nothing being modified)
const_cast<foo&>(y).bar() = 5; // calls non-const, which calls const,
// removes const from result, and
// modifies which is undefined because
// the object pointed to by the returned
// reference is const, because y is const.
y.baz(); // well-formed, always undefined
}
Я имею в виду стандарт ISO C ++ 03.
ИМО, вы не делаете ничего технически неправильного.Может быть, было бы проще понять, был ли член указателем.
class X
{
Y* m_ptr;
void foo() const {
m_ptr = NULL; //illegal
*m_ptr = 42; //legal
}
};
const
делает указатель константой, а не указатель .
Обратите внимание на разницу между:
const X* ptr;
X* const ptr; //this is what happens in const member functions
Что касается ссылок, так как они все равно не могут быть переустановлены, ключевое слово const
в методе не оказывает никакого влияния на ссылочные элементы.
В вашем примере я не вижу никаких константных объектов, поэтому вы не делаете ничего плохого, а просто используете странную лазейку в том, как корректность констант работает в C ++.
Не вдаваясь в подробности, можно ли / следует / можно ли это разрешить, я бы очень посоветовал этого не делать. В языке есть механизмы того, чего вы хотите достичь, которые не требуют написания непонятных конструкций, которые, скорее всего, запутают других разработчиков.
Посмотрите на ключевое слово изменяемое
. Это ключевое слово можно использовать для объявления членов, которые могут быть изменены в методах членов const
, поскольку они не влияют на воспринимаемое состояние класса. Рассмотрим класс, который инициализируется набором параметров и выполняет сложные дорогостоящие вычисления, которые могут не требоваться всегда:
class ComplexProcessor
{
public:
void setInputs( int a, int b );
int getValue() const;
private:
int complexCalculation( int a, int b );
int result;
};
Возможная реализация - это добавление результирующего значения в качестве члена и вычисление его для каждого набора:
void ComplexProcessor::setInputs( int a, int b ) {
result = complexCalculation( a, b );
}
Но это означает что значение рассчитывается во всех наборах, независимо от того, нужно это или нет.Если вы думаете об объекте как о черном ящике, интерфейс просто определяет метод для установки параметров и метод для получения вычисленного значения. Момент, когда выполняется вычисление, на самом деле не влияет на воспринимаемое состояние объекта - поскольку значение, возвращаемое геттером, является правильным. Таким образом, мы можем изменить класс для хранения входных данных (вместо выходных) и вычислять результат только тогда, когда это необходимо:
class ComplexProcessor2 {
public:
void setInputs( int a, int b ) {
a_ = a; b_ = b;
}
int getValue() const {
return complexCalculation( a_, b_ );
}
private:
int complexCalculation( int a, int b );
int a_,b_;
};
Семантически второй класс и первый класс эквивалентны, но теперь мы избежали выполнения сложных вычислений, если value не требуется, поэтому рекомендуется, чтобы значение запрашивалось только в некоторых случаях. Но в то же время недостатком является то, что значение запрашивается много раз для одного и того же объекта: каждый раз будет выполняться сложный расчет, даже если входные данные не изменились.
Решение - кеширование результата. Для этого мы можем результат классу. Когда запрашивается результат, если мы уже вычислили его, нам нужно только получить его, а если у нас нет значения, мы должны его вычислить. При изменении входных данных мы аннулируем кеш. Здесь пригодится ключевое слово изменяемое
. Он сообщает компилятору, что член не является частью воспринимаемого состояния и как таковой он может быть изменен в рамках постоянного метода:
class ComplexProcessor3 {
public:
ComplexProcessor3() : cached_(false) {}
void setInputs( int a, int b ) {
a_ = a; b_ = b;
cached_ = false;
}
int getValue() const {
if ( !cached_ ) {
result_ = complexCalculation( a_, b_ );
cached_ = true;
}
return result_;
}
private:
int complexCalculation( int a, int b );
int a_,b_;
// This are not part of the perceivable state:
mutable int result_;
mutable bool cached_;
};
Третья реализация семантически эквивалентна двум предыдущим версиям, но избегает необходимости пересчитывать значение, если результат уже известен - и закеширован.
Ключевое слово mutable
необходимо в других местах, например, в многопоточных приложениях, мьютекс в классах часто помечается как mutable
. Блокировка и разблокировка мьютекса - это мутирующие операции для мьютекса: его состояние явно меняется. Теперь метод получения в объекте, который совместно используется разными потоками, не изменяет воспринимаемое состояние, но должен получать и снимать блокировку, если операция должна быть поточно-ориентированной:
template <typename T>
class SharedValue {
public:
void set( T v ) {
scoped_lock lock(mutex_);
value = v;
}
T get() const {
scoped_lock lock(mutex_);
return value;
}
private:
T value;
mutable mutex mutex_;
};
Операция получения является семантически постоянной, даже если она требует для изменения мьютекса для обеспечения однопоточного доступа к члену value
.
Ключевое слово const
учитывается только во время проверок времени компиляции. C ++ не предоставляет средств для защиты вашего класса от любого доступа к памяти, что вы делаете с указателем / ссылкой. Ни компилятор, ни среда выполнения не могут знать, указывает ли ваш указатель на экземпляр, который вы где-то объявили как const.
РЕДАКТИРОВАТЬ:
Краткий пример (возможно, не компилируется):
// lets say foo has a member const int Foo::datalength() const {...}
// and a read only acces method const char data(int idx) const {...}
for (int i; i < foo.datalength(); ++i)
{
foo.questionable(); // this will most likely mess up foo.datalength !!
std::cout << foo.data(i); // HERE BE DRAGONS
}
В этом случае компилятор может решить, ey, foo.datalength is const, и код внутри цикла обещал не изменять foo, поэтому я должен оценить datalength только один раз, когда я вхожу в цикл. Йиппи! И если вы попытаетесь отладить эту ошибку, которая, скорее всего, появится только при компиляции с оптимизацией (не в отладочных сборках), вы сведете себя с ума.
Выполняйте обещания! Или используйте mutable с вашими мозгами в состоянии повышенной готовности!
Вы достигли круговой зависимости. См. FAQ 39.11 И да, изменение const
данных является UB, даже если вы обошли компилятор. Кроме того, вы сильно ухудшаете способность компилятора к оптимизации, если не выполняете свои обещания (читай: нарушаете const
).
Почему Questionable
const
, если вы знаете, что измените его через обращение к его владельцу? Зачем объекту-владельцу нужно знать о владельце? Если вам действительно нужно это сделать, то mutable
- это то, что нужно. Именно для этого он и существует - для логической константности (в отличие от строгой константности на уровне битов).
Из моей копии проекта n3090:
9.3.2 Указатель this [class.this]
1 В теле нестатической (9.3) функции-члена ключевое слово this является выражением rvalue a prvalue, чье значением является адрес объекта, для которого вызывается функция. Тип this в функции-члене класса X является X*. Если функция-член объявлена const, то тип this - const X*, если функция-член объявлена volatile, тип this - volatile X*, а если функция-член объявлена const volatile, то тип этой функции - const volatile X*.
2 В функции-члене const доступ к объекту, для которого вызывается функция, осуществляется через доступ const поэтому функция-член const не должна изменять объект и его нестатические члены данных.
[Примечание выделено мной].
На UB:
7.1.6.1 Cv-квалификаторы
3 Указатель или ссылка на cv-квалифицированный тип не обязательно должен фактически указывать или ссылаться на cv-квалифицированный объект, но он рассматривается так, как если бы он так и есть; const-квалифицированный путь доступа не может быть использован для модификации объекта даже если объект, на который ссылаются, является неконст-объект и может быть изменен через другой путь доступа. [ Примечание: cv-квалификаторы поддерживаются системой типов так, что они не могут быть не могут быть изменены без приведения (5.2.11). -конец примечания ]
4 За исключением того, что любой класс объявленный как изменяемый (7.1.1), может быть может быть изменен, любая попытка изменить const объект во время его жизни (3.8) приводит к неопределенному поведению.