С++: проблемы с STL с членами класса const

Это открытый вопрос. Эффективный С++. Пункт 3. Использовать константу, когда это возможно. Действительно?

Я хотел бы сделать все, что не меняется во время жизни const. Но состретется со своими собственными проблемами. Если класс имеет какой-либо константный член, оператор сгенерированного оператора компилятора отключается. Без оператора присваивания класс не будет работать с STL. Если вы хотите предоставить свой собственный оператор присваивания, требуется const_cast. Это означает больше шума и больше места для ошибок. Как часто вы используете члены класса const?

РЕДАКТИРОВАТЬ: Как правило, я стремлюсь к константной корректности, потому что много многопоточности. Мне редко приходится реализовывать контроль копирования для моих классов и никогда не удалять код (если это абсолютно необходимо). Я чувствую, что текущее состояние дел с const противоречит моему стилю кодирования. Const заставляет меня реализовать оператор присваивания, хотя он мне не нужен. Даже без const_cast назначение - это хлопот. Вы должны убедиться, что все члены const сравниваются с равными, а затем вручную копируют все неконстантные элементы.

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

class Multiply {
public:
    Multiply(double coef) : coef_(coef) {}
    double operator()(double x) const {
        return coef_*x;
    }
private:
    const double coef_;
};

Ответы

Ответ 1

Как отметил AndreyT, в этих условиях назначение (в основном) не имеет большого смысла. Проблема в том, что vector (для одного примера) является скорее исключением из этого правила.

Логически, вы копируете объект в vector, а через некоторое время вы возвращаете другую копию исходного объекта. С чисто логической точки зрения, никакого назначения не было. Проблема в том, что vector требует, чтобы объект был назначен в любом случае (на самом деле все контейнеры С++). Это в основном делает деталь реализации (что где-то в своем коде, он может назначать объекты, а не копировать их) часть интерфейса.

Для этого нет простого лечения. Даже определение вашего собственного оператора присваивания и использование const_cast на самом деле не устраняет проблему. Совершенно безопасно использовать const_cast, когда вы получаете указатель const или ссылку на объект, который, как вы знаете, на самом деле не определен const. В этом случае, однако, сама переменная определяется как const - попытка отбросить const ness и присвоить ей поведение undefined. В действительности, он почти всегда будет работать в любом случае (до тех пор, пока он не static const с инициализатором, который известен во время компиляции), но нет никакой гарантии.

С++ 11 и новее добавьте несколько новых поворотов в эту ситуацию. В частности, объекты больше не должны быть назначены для хранения в векторе (или других коллекциях). Достаточно, чтобы они были подвижными. Это не помогает в этом конкретном случае (перемещать объект const не проще, чем назначать его), но делает жизнь значительно проще в некоторых других случаях (т.е. Есть, конечно, типы, которые могут быть подвижными, но не назначаемыми/копируемый).

В этом случае вы можете использовать перемещение, а не копию, добавляя уровень косвенности. Если вы создаете "внешний" и "внутренний" объект, с элементом const во внутреннем объекте, а внешний объект просто содержит указатель на внутренний:

struct outer { 
    struct inner {
        const double coeff;
    };

    inner *i;
};

... тогда, когда мы создаем экземпляр outer, мы определяем объект inner для хранения данных const. Когда нам нужно выполнить задание, мы выполняем типичное назначение перемещения: скопируйте указатель со старого объекта на новый и (возможно) установите указатель в старом объекте на nullptr, поэтому, когда он будет уничтожен, t попытаться уничтожить внутренний объект.

Если вы хотите достаточно сильно, вы можете использовать (вид) тот же метод в более старых версиях С++. Вы все равно будете использовать внешние/внутренние классы, но каждое присваивание будет выделять целый новый внутренний объект, или вы будете использовать что-то вроде shared_ptr, чтобы внешние экземпляры могли обмениваться доступом к одному внутреннему объекту и очищать его, когда последний наружный объект уничтожен.

Это не имеет никакого реального значения, но, по крайней мере, для назначения, используемого при управлении вектором, у вас будет только две ссылки на inner, а vector будет изменять размер (изменение размера - это почему вектор требует назначения для начала с).

Ответ 2

Вы сами сказали, что вы создаете const "все, что не меняется во время жизни объектов". Тем не менее вы жалуетесь, что оператор неявно объявленного присваивания отключается. Но неявно объявленный оператор присваивания меняет содержимое члена, о котором идет речь! Совершенно логично (по вашей собственной логике), что он отключается. Либо это, либо вы не должны объявлять этот член const.

Кроме того, для предоставления вам собственного оператора присваивания не требуется const_cast. Зачем? Вы пытаетесь присвоить члену, который вы объявили const внутри вашего оператора присваивания? Если да, то почему вы объявили его const?

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

Ответ 3

Я очень редко их использую - хлопот слишком велик. Конечно, я всегда стремлюсь к константной корректности, когда речь идет о функциях члена, параметрах или типах возврата.

Ответ 4

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

Ответ 5

Я стараюсь следовать советам по использованию const, когда это возможно, однако я согласен, что когда дело доходит до членов класса, const - большая проблема.

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

Ответ 6

Мне интересно о вашем случае... Все ниже - это предположение, потому что вы не представили пример кода, описывающего вашу проблему, поэтому...

Причина

Я думаю, у вас есть что-то вроде:

struct MyValue
{
   int         i ;
   const int   k ;
} ;

IIRC, оператор присваивания по умолчанию будет выполнять назначение по каждому члену, что сродни:

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   this->k = rhs.k ; // THIS WON'T WORK BECAUSE K IS CONST
   return *this ;
} ;

Таким образом, это не будет генерироваться.

Итак, ваша проблема заключается в том, что без этого оператора присваивания контейнеры STL не будут принимать ваш объект.

Насколько я вижу это:

  • Компилятор прав, чтобы не генерировать этот operator =
  • Вы должны предоставить свои собственные, потому что только вы точно знаете, что хотите.

Ваше решение

Я боюсь понять, что вы подразумеваете под const_cast.

Мое собственное решение вашей проблемы состояло в том, чтобы написать следующий пользовательский оператор:

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   // DON'T COPY K. K IS CONST, SO IT SHOULD NO BE MODIFIED.
   return *this ;
} ;

Таким образом, если у вас будет:

MyValue a = { 1, 2 }, b = {10, 20} ;
a = b ; // a is now { 10, 2 } 

Насколько я вижу, он когерентен. Но я думаю, читая решение const_cast, что вы хотите иметь что-то большее:

MyValue a = { 1, 2 }, b = {10, 20} ;
a = b ; // a is now { 10, 20 } :  K WAS COPIED

Это означает следующий код для operator =:

MyValue & operator = (const MyValue & rhs)
{
   this->i = rhs.i ;
   const_cast<int &>(this->k) = rhs.k ;
   return *this ;
} ;

Но тогда вы написали в своем вопросе:

Я хотел бы сделать все, что не меняется во время жизни объекта const

С тем, что я считаю вашим собственным решением const_cast, k изменилось во время жизни объекта, что означает, что вы противоречите себе, потому что вам нужна переменная-член, которая не изменяется во время жизни объекта , если только вы хотите, чтобы он менял!

Решение

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

Ответ 7

вы можете хранить shared_ptr для объектов const в контейнерах STL, если вы хотите сохранить const членов.

#include <iostream>

#include <boost/foreach.hpp>
#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/utility.hpp>

#include <vector>

class Fruit : boost::noncopyable
{
public:
    Fruit( 
            const std::string& name
         ) :
        _name( name )
    {

    }

    void eat() const { std::cout << "eating " << _name << std::endl; }

private:
    const std::string _name;
};

int
main()
{
    typedef boost::shared_ptr<const Fruit> FruitPtr;
    typedef std::vector<FruitPtr> FruitVector;
    FruitVector fruits;
    fruits.push_back( boost::make_shared<Fruit>("apple") );
    fruits.push_back( boost::make_shared<Fruit>("banana") );
    fruits.push_back( boost::make_shared<Fruit>("orange") );
    fruits.push_back( boost::make_shared<Fruit>("pear") );
    BOOST_FOREACH( const FruitPtr& fruit, fruits ) {
        fruit->eat();
    }

    return 0;
}

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

Ответ 8

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

Лучшие места для использования const - это параметры функции, указатели и ссылки всех видов, постоянные целые числа и временные удобные значения.

Примером временной переменной удобства будет:

char buf[256];
char * const buf_end = buf + sizeof(buf);
fill_buf(buf, buf_end);
const size_t len = strlen(buf);

Указатель buf_end никогда не должен указывать в другом месте, поэтому создание его является хорошей идеей. Та же идея с len. Если строка внутри buf никогда не изменяется в остальной части функции, то ее len также не следует изменять. Если бы я мог, я бы даже изменил buf на const после вызова fill_buf, но C/С++ не позволяет вам это делать.

Ответ 9

Дело в том, что плакат хочет const защиты в своей реализации, но все же хочет, чтобы объект был назначен. Язык не поддерживает такую ​​семантику удобно, поскольку константа элемента находится на одном и том же логическом уровне и тесно связана с назначаемостью.

Тем не менее, идиома pImpl со ссылкой на подсчитанную реализацию или умный указатель будет делать именно то, что хочет плакат, поскольку назначаемость затем переносится из реализации и выше уровня на объект более высокого уровня. Объект реализации создается или разрушается только тогда, когда назначение никогда не требуется на более низком уровне.

Ответ 10

Я думаю, ваше выражение

Если класс имеет const любой член, оператор сгенерированного компилятора отключен.

Возможно, неверно. У меня есть классы, которые имеют метод const

bool is_error(void) const;
....
virtual std::string info(void) const;
....

которые также используются с STL. Итак, возможно, ваше наблюдение зависит от компилятора или применимо только к переменным-членам?

Ответ 11

Это не слишком сложно. У вас не должно возникнуть проблем с созданием собственного оператора присваивания. Константные биты не нужно назначать (поскольку они являются константами).

Обновление
Существует некоторое недоразумение в отношении того, что означает const. Это означает, что он никогда не изменится.

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

class CTheta
{
public:
    CTheta(int nVal)
    : m_nVal(nVal), m_pi(3.142)
    {
    }
    double GetPi() const { return m_pi; }
    int GetVal()   const { return m_nVal; }
    CTheta &operator =(const CTheta &x)
    {
        if (this != &x)
        {
            m_nVal = x.GetVal();
        }
        return *this;
    }
private:
    int m_nVal;
    const double m_pi;
};

bool operator < (const CTheta &lhs, const CTheta &rhs)
{
    return lhs.GetVal() < rhs.GetVal();
}
int main()
{
    std::vector<CTheta> v;
    const size_t nMax(12);

    for (size_t i=0; i<nMax; i++)
    {
        v.push_back(CTheta(::rand()));
    }
    std::sort(v.begin(), v.end());
    std::vector<CTheta>::const_iterator itr;
    for (itr=v.begin(); itr!=v.end(); ++itr)
    {
        std::cout << itr->GetVal() << " " << itr->GetPi() << std::endl;
    }
    return 0;
}

Ответ 12

Я бы использовал только член const, если сам класс не копируется. У меня есть много классов, которые я объявляю с boost:: noncopyable

class Foo : public boost::noncopyable {
    const int x;
    const int y;
}

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

#include <new>
#include <iostream>
struct Foo {
    Foo(int x):x(x){}
    const int x;
    friend std::ostream & operator << (std::ostream & os, Foo const & f ){
         os << f.x;
         return os;
    }
};

int main(int, char * a[]){
    Foo foo(1);
    Foo bar(2);
    std::cout << foo << std::endl;
    std::cout << bar<< std::endl;
    new(&bar)Foo(foo);
    std::cout << foo << std::endl;
    std::cout << bar << std::endl;

}

выходы

1
2
1
1

foo был скопирован в бар с использованием оператора нового места размещения.

Ответ 13

Философски говоря, это выглядит как компромисс в области безопасности. Const используется для безопасности. Насколько я понимаю, контейнеры используют назначение для повторного использования памяти, то есть ради производительности. Вместо этого они могут использовать явное уничтожение и размещение новых (и логично, что это более правильно), но назначение имеет шанс быть более эффективным. Я полагаю, это логически избыточное требование "быть назначаемым" (достаточно для создания копии), но контейнеры stl хотят быть быстрее и проще.

Конечно, можно реализовать назначение как явное уничтожение + размещение new, чтобы избежать const_cast hack

Ответ 14

В основном вы никогда не хотите вставлять переменную-член const в класс. (То же самое с использованием ссылок в качестве членов класса.)

Константа действительно предназначена для вашего потока управления программой - чтобы предотвратить неправильное изменение объектов в коде. Поэтому не объявляйте константные членские переменные в определении класса, а делайте все это или ничего, когда вы объявляете экземпляры класса.