Const метод, который изменяет * это без const_cast - PullRequest
9 голосов
/ 14 августа 2010

Следующая схема возникла в программе, которую я пишу.Я надеюсь, что это не слишком надумано, но ему удается мутировать объект 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(), this имеет тип 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;
    }
};

Ответы [ 5 ]

25 голосов
/ 15 августа 2010

Рассмотрим следующее:

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. 5.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:

За исключением того, что любой член класса, объявленный mutable (7.1.1), может быть изменен, любая попытка изменить объект const в течение срока его службы (3.8) приводит к неопределенному поведению . Акцент мой.

Помните, что i - это , а не const, и что j и k оба указывают на i. Все, что мы сделали, это сказали системе типов удалить константный квалификатор из типа, чтобы мы могли изменить указанный объект, а затем изменить 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 на bar делает с участниками? Это позволяет получить доступ к ним через то, что называется 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?
}

В пределах bar оба значения me и self указывают на неконстантное значение foo, поэтому, как и в случае с int i, мы имеем четко определенное поведение. Если бы у нас было:

const foo f;
f.bar(); // UB!

У нас был бы UB, как и с const int, потому что мы бы модифицировали объект с квалификацией const.

В вашем вопросе у вас нет константных объектов, поэтому у вас нет неопределенного поведения.


И просто добавьте обращение к авторитету, рассмотрим уловку const_cast Скотта Мейерса, использованную для утилизации функции, удовлетворяющей const, в неконстантной функции:

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
            ); 
}

NЭто опять-таки совершенно законно и четко определено. В частности, поскольку эта функция должна вызываться для неконстантной 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.

6 голосов
/ 14 августа 2010

ИМО, вы ничего не делаете технически неправильно.Возможно, было бы проще понять, если бы элемент был указателем.

class X
{
    Y* m_ptr;
    void foo() const {
        m_ptr = NULL; //illegal
        *m_ptr = 42; //legal
    }
};

const делает указатель const, а не pointee .

Рассмотрим разницу между:

const X* ptr;
X* const ptr;  //this is what happens in const member functions

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

В вашем примере я не вижу никаких const-объектов, поэтому вы не делаете ничего плохого, просто используете странную лазейку в том, как работает правильность const в C ++.

1 голос
/ 15 августа 2010

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

Просмотрите ключевое слово mutable. Это ключевое слово может использоваться для объявления членов, которые могут быть изменены в 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_;
};

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

Решение кеширует результат. Для этого мы можем привести к классу. Когда запрашивается результат, если мы уже вычислили его, нам нужно только получить его, а если у нас нет значения, мы должны его вычислить. Когда входные данные изменяются, мы делаем недействительным кеш. Это когда ключевое слово mutable пригодится. Он сообщает компилятору, что член не является частью воспринимаемого состояния и, как таковой, его можно изменить в константном методе:

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.

0 голосов
/ 14 августа 2010

Ключевое слово const учитывается только при проверках времени компиляции. C ++ не предоставляет никаких средств для защиты вашего класса от любого доступа к памяти, что вы и делаете со своим указателем / ссылкой. Ни компилятор, ни среда выполнения не могут знать, указывает ли ваш указатель на экземпляр, который вы где-то объявили как const.

EDIT:

Краткий пример (может не скомпилироваться):

// 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
}

В этом случае компилятор может решить, что у, foo.datalength is const, и код внутри цикла обещал не изменять foo, поэтому я должен оценить Длина данных только один раз, когда я вхожу в цикл. Yippie! И если вы попытаетесь отладить эту ошибку, которая, скорее всего, появится, только если вы скомпилируете с оптимизацией (а не в отладочных сборках), вы сведете с ума.

Выполните обещания! Или используйте изменчивый с вашими мозговыми ячейками в состоянии повышенной готовности!

0 голосов
/ 14 августа 2010

Вы достигли круговых зависимостей.См. FAQ 39.11 И да, изменение const данных - это UB, даже если вы обошли компилятор.Кроме того, вы сильно ухудшаете способность компилятора оптимизировать, если вы не выполняете свои обещания (читай: нарушать const).

Почему Questionable const, если вы знаете, что измените его, позвонив его владельцу?Почему принадлежащий объект должен знать о владельце?Если вам действительно нужно это сделать, тогда mutable - это путь.Вот для чего это - логическая константа (в отличие от строгой константности на уровне битов).

Из моей копии черновика n3090:

9.3.2Указатель this [class.this]

1 В теле нестатической (9.3) функции-члена ключевое слово this является rvalue выражением prvalue, значение которого равноадрес объекта, для которого вызывается функция.Тип этого в функции-члене класса X - X *. Если функция-член объявлена ​​как const, типом этого является const X *, если функция-член объявлена ​​как volatile, типом является volatile X *, и если членфункция объявлена ​​как const volatile, ее тип является постоянным volatile X *.

2 В функции-члене const объект, для которого вызывается функция, доступен через путь доступа к const;следовательно, функция-член const не должна изменять объект и его нестатические члены-данные.

[Обратите внимание на мое].

На UB:

7.1.6.1 Спецификаторы cv

3 Указатель или ссылка на тип, квалифицированный cv, не обязательно должен указывать или ссылаться на объект, квалифицированный cv, но это трактуется так, как будто это так;квалифицированный константный путь доступа не может использоваться для изменения объекта, даже если упомянутый объект является неконстантным объектом и может быть изменен через какой-либо другой путь доступа.[Примечание: cv-квалификаторы поддерживаются системой типов, поэтому они не могут быть преобразованы без приведения (5.2.11).—Конец примечания]

4 За исключением того, что любой член класса, объявленный изменяемым (7.1.1), может быть изменен, любая попытка изменить объект const в течение его времени жизни (3.8) приводит к неопределенному поведению.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...