簡體   English   中英

C ++:STL使用const類成員時遇到麻煩

[英]C++: STL troubles with const class members

這是一個開放式的問題。 有效的C ++。 第3項。 盡可能使用const。 真?

我想做一些在對象生命周期const期間不會改變的東西。 但const帶來了它自己的麻煩。 如果類具有任何const成員,則禁用編譯器生成的賦值運算符。 如果沒有賦值運算符,則類將無法與STL一起使用。 如果要提供自己的賦值運算符,則需要const_cast 這意味着更多的喧囂和更多的錯誤空間。 你經常使用const類成員?

編輯:作為一項規則,我努力保持const的正確性,因為我做了很多多線程。 我很少需要為我的類實現復制控制,從不編寫刪除代碼(除非絕對必要)。 我覺得const的當前狀態與我的編碼風格相矛盾。 Const迫使我實現賦值運算符,即使我不需要它。 即使沒有const_cast分配也很麻煩。 您需要確保所有const成員比較相等,然后手動復制所有非const成員。

碼。 希望它能澄清我的意思。 您在下面看到的課程不適用於STL。 您需要為它實現一個賦值,即使您不需要它。

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

你自己說過你使const“在物體生命期間不會改變的任何東西”。 然而,您抱怨隱式聲明的賦值運算符被禁用。 但隱式聲明的賦值運算符確實改變了有問題的成員的內容! 完全合乎邏輯(根據您自己的邏輯),它正在被禁用。 要么是,要么你不應該聲明成員const。

此外,提供自己的賦值運算符不需要const_cast 為什么? 您是否嘗試在賦值運算符中指定您聲明為const的成員? 如果是這樣,為什么你聲明它為const呢?

換句話說,提供您正在遇到的問題的更有意義的描述。 到目前為止你提供的那個以最明顯的方式是自相矛盾的。

我很少使用它們 - 麻煩太大了。 當然,在成員函數,參數或返回類型方面,我總是力求const正確性。

正如AndreyT指出的那樣,在這些情況下,分配(大多數)並沒有多大意義。 問題是vector (例如)是該規則的一個例外。

從邏輯上講,您將對象復制到vector ,稍后您將獲得原始對象的另一個副本。 從純粹的邏輯觀點來看,不涉及任務。 問題是vector要求對象無論如何都是可分配的(實際上,所有C ++容器都可以)。 它基本上是一個實現細節(在代碼中的某個地方,它可能分配對象而不是復制它們)的一部分接口。

對此沒有簡單的治療方法。 即使定義自己的賦值運算符並使用const_cast也無法解決問題。 當你獲得一個const指針或對一個你知道實際上沒有被定義為const的對象的引用時,使用const_cast是完全安全的。 但是,在這種情況下,變量本身定義為const - 試圖拋棄const並賦予它給出未定義的行為。 實際上,它幾乎總是可以工作(只要它不是帶有在編譯時已知的初始化器的static const ),但是不能保證它。

C ++ 11和更新版本為這種情況添加了一些新的曲折。 特別是,不再需要將對象分配以存儲在向量(或其他集合)中。 它們可以移動就足夠了。 這在這種特殊情況下沒有幫助(移動const對象比分配它更容易)但是在其他一些情況下確實使生活變得更加容易(例如,肯定有可移動但不可分配/可復制的類型)。

在這種情況下,您可以通過添加間接級別使用移動而不是副本。 如果你創建一個“外部”和一個“內部”對象,內部對象中的const成員和外部對象只包含指向內部的指針:

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

    inner *i;
};

...然后當我們創建一個outer實例時,我們定義一個inner對象來保存const數據。 當我們需要做一個賦值時,我們做一個典型的移動賦值:將指針從舊對象復制到新對象,並且(可能)將舊對象中的指針設置為nullptr,所以當它被銷毀時,它就贏了試着破壞內部物體。

如果你想要足夠嚴重,你可以在舊版本的C ++中使用(類似)相同的技術。 您仍然使用外部/內部類,但每個賦值將分配一個全新的內部對象,或者您使用類似shared_ptr的東西讓外部實例共享對單個內部對象的訪問權限,並在最后一個外部物體被摧毀

它沒有任何真正的區別,但至少對於管理向量時使用的賦值,在vector調整自身時,你只有兩個對inner引用(調整大小是為什么向量需要賦值可以開始)。

編譯時的錯誤很痛苦,但運行時的錯誤是致命的。 使用const的構造可能是代碼的麻煩,但它可能會幫助您在實現它們之前找到錯誤。 我盡可能使用consts。

我盡可能地遵循使用const的建議,但我同意,當涉及到班級成員時, const是一個很大的麻煩。

我發現在參數方面我非常小心const -correctness,但對於類成員卻沒有那么多。 實際上,當我使類成員const並且它導致錯誤時(由於使用STL容器),我做的第一件事就是刪除const

我想知道你的情況......以下所有內容都是假設,因為你沒有提供描述你問題的示例代碼,所以...

原因

我想你有類似的東西:

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容器將不接受你的對象。

就我所見:

  1. 編譯器是不正確生成此operator =
  2. 你應該提供自己的,因為只有你知道你想要什么

你解決方案

我害怕明白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在對象生命周期中發生了變化,這意味着你自相矛盾,因為你需要一個在對象生命周期內不會改變的成員變量,

解決方案

接受您的成員變量在其所有者對象的生命周期內將更改的事實,並刪除co​​nst。

如果您想保留const成員,可以將shared_ptr存儲到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;
}

但是,正如其他人已經指出的那樣,如果你想要編譯器生成的復制構造函數,那么在我看來刪除const限定成員往往更容易。

我只在引用或指針類成員上使用const。 我用它來表明不應該改變引用或指針的目標。 你發現,在其他類別的成員上使用它是一個很大的麻煩。

使用const的最佳位置是函數參數,指針和各種引用,常量整數和臨時便利值。

臨時便利變量的一個例子是:

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

那個buf_end指針永遠不應該指向其他任何地方,所以使它成為const是一個好主意。 len相同的想法。 如果buf的字符串在函數的其余部分中永遠不會改變,那么它的len也不應該改變。 如果可以,我甚至會在調用fill_buf之后將buf更改為const,但C / C ++不會讓你這樣做。

關鍵是海報希望在他的實現中保持const但仍然希望對象可分配。 該語言不方便地支持這種語義,因為成員的constness位於相同的邏輯級別並且與可賦值性緊密耦合。

然而,具有引用計數實現或智能指針的pImpl慣用語將完全符合海報的要求,因為可分配性隨后被移出實現並向更高級別對象上升。 實現對象僅被構造/破壞,從而在較低級別永遠不需要賦值。

我想你的說法

如果類具有const任何成員,則禁用編譯器生成的賦值運算符。

可能不對。 我有使用const方法的類

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

也用於STL。 那么您的觀察結果可能依賴於編譯器還是僅適用於成員變量?

這不是太難。 制作自己的賦值運算符不會有任何問題。 不需要分配const位(因為它們是const)。

更新
關於const的含義存在一些誤解。 這意味着它永遠不會改變。

如果一個賦值應該改變它,那么它不是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;
}

我只會使用const成員iff類本身是不可復制的。 我用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

已使用placement new運算符將foo復制到bar。

從哲學上講,它看起來像安全性能權衡。 Const用於安全。 據我所知,容器使用賦值來重用內存,即為了性能。 他們可能會使用顯式銷毀和替換新的替代(並且邏輯上它更正確),但是賦值有更高效的機會。 我認為,邏輯冗余要求“可分配”(復制可構造就足夠了),但是stl容器希望更快更簡單。

當然,可以將賦值實現為顯式destroy + placement new以避免const_cast hack

你基本上不想把一個const成員變量放在一個類中。 (同上使用引用作為類的成員。)

Constness真正用於程序的控制流程 - 防止在代碼中錯誤的時間發生變異。 因此,不要在類的定義中聲明const成員變量,而是在聲明類的實例時全部或全部使用它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM