Const метод, который изменяет * это без const_cast

В программе, которую я пишу, возник следующий шаблон. Я надеюсь, что он не слишком надуман, но ему удается мутировать объект 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;
    }
};
9
задан AshleysBrain 14 August 2010 в 20:57
поделиться

5 ответов

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

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.

25
ответ дан 4 December 2019 в 07:13
поделиться

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

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 ++.

6
ответ дан 4 December 2019 в 07:13
поделиться

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

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

1
ответ дан 4 December 2019 в 07:13
поделиться

Ключевое слово 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 с вашими мозгами в состоянии повышенной готовности!

0
ответ дан 4 December 2019 в 07:13
поделиться

Вы достигли круговой зависимости. См. 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) приводит к неопределенному поведению.

-1
ответ дан 4 December 2019 в 07:13
поделиться
Другие вопросы по тегам:

Похожие вопросы: