2010-07-30 33 views
26

Cách thích hợp để thực hiện phương thức getter cho biến thành viên được khởi tạo lười biếng và duy trì độ chính xác là gì? Tức là, tôi sẽ như để có phương thức getter của tôi là const, bởi vì sau lần đầu tiên nó được sử dụng, đó là một phương thức getter bình thường. Nó chỉ là lần đầu tiên (khi đối tượng được khởi tạo lần đầu tiên) mà const không áp dụng. Những gì tôi muốn làm:Phương pháp getter C++ const với khởi tạo lười biếng

class MyClass { 
    MyClass() : expensive_object_(NULL) {} 
    QObject* GetExpensiveObject() const { 
    if (!expensive_object_) { 
     expensive_object = CreateExpensiveObject(); 
    } 
    return expensive_object_; 
    } 
private: 
    QObject *expensive_object_; 
}; 

Tôi có thể ăn bánh của mình không?

Trả lời

17

Tôi đề nghị đóng gói câu trả lời James Curran 's vào một lớp học riêng của mình nếu bạn làm điều này thường xuyên:

template <typename T> 
class suspension{ 
    std::tr1::function<T()> initializer; 
    mutable T value; 
    mutable bool initialized; 
public: 
    suspension(std::tr1::function<T()> init): 
     initializer(init),initialized(false){} 
    operator T const &() const{ 
     return get(); 
    } 
    T const & get() const{ 
     if (!initialized){ 
     value=initializer(); 
     initialized=true; 
     } 
     return value; 
    } 
}; 

Bây giờ sử dụng điều này trong mã của bạn như sau:

class MyClass { 
    MyClass() : expensive_object_(CreateExpensiveObject) {} 
    QObject* GetExpensiveObject() const { 
    return expensive_object_.get(); 
    } 
private: 
    suspension<QObject *> expensive_object_; 
}; 
+0

Tôi cũng thích câu trả lời của bạn. Việc triển khai giao diện con trỏ có thể nhất quán hơn giao diện tham chiếu. –

+0

Điều này dường như là giải pháp phổ biến nhất "chơi đẹp" với thông số C++. Đó là xấu xí dưới bề mặt, nhưng API công cộng vẫn sạch sẽ. –

+1

Tôi ngạc nhiên vì tôi không thể tìm thấy thư viện Boost cho việc này. –

21

Tốt và là cách làm điển hình.

Bạn sẽ phải khai báo expensive_object_ như mutable

mutable QObject *expensive_object_; 

mutable về cơ bản có nghĩa là "Tôi biết tôi đang ở trong một đối tượng const, nhưng việc sửa đổi này sẽ không phá vỡ const-Ness."

+1

@ Jon-Eric: Nếu nó không thực sự là const, nó sẽ có thể thay đổi. Có một const_cast ở một chỗ không thể hiện rõ điều đó. –

+1

@David: Việc nhận của tôi là nó thực sự là IS const và một chỗ là nơi duy nhất nó sẽ được sửa đổi (khởi tạo). Nếu nhiều phương pháp/địa điểm cần phải sửa đổi nó (như bộ nhớ cache), thì có thể biến đổi sẽ có ý nghĩa hơn. –

+5

Chúng ta đang nói về các chi tiết thực hiện của một biến có thể được bảo vệ/riêng tư.Có vấn đề gì nếu các phương pháp khác cũng có thể thay đổi nó? Các phương pháp khác cũng có thể rút ra một const cast và thay đổi nó quá. Không có gói bổ sung nào bằng cách không sử dụng được. Tuy nhiên, làm cho nó có thể thay đổi nói cho người đọc về định nghĩa của lớp một cái gì đó có giá trị để biết. – frankc

6

Làm cho expensive_object_ có thể thay đổi.

4

Sử dụng một số const_cast tới bước bên cạnh trong một địa điểm cụ thể đó.

QObject* GetExpensiveObject() const { 
    if (!expensive_object_) { 
    const_cast<QObject *>(expensive_object_) = CreateExpensiveObject(); 
    } 
    return expensive_object_; 
} 

IMHO, đây là tốt hơn so với làm expensive_object_mutable bởi vì bạn không mất const-an toàn trong tất cả các phương pháp khác của bạn.

+1

Tại sao downvote? –

+0

Đồng ý. Trong khi tôi _do_ grep codebase cho const_cast và C-style phôi định kỳ để đảm bảo không có vi phạm const, đây là trường hợp tôi nghĩ nó sẽ là chính đáng. (Mặc dù tôi không chắc chắn tôi đồng ý với -1 của bạn về câu trả lời có thể thay đổi được ... Chúng đều là cách tốt để xử lý sự cố, chỉ với các hậu quả khác nhau: cú pháp mã đáng báo động so với nhiều biến đổi hơn mức cần thiết. ..) – leander

+2

Tôi không phải là downvoter, nhưng tôi tin rằng điều này có thể dẫn đến hành vi undefined nếu dụ MyClass được tạo ra như 'const'. –

3

Bạn đã xem là một lớp bao bọc? Bạn có thể thoát khỏi một thứ gì đó giống như con trỏ thông minh, chỉ với các phiên bản trả về của operator*operator-> và có thể operator[] ... Bạn có thể nhận được scoped_ptr hành vi giống như tiền thưởng.

Hãy cung cấp cho một shot, tôi chắc chắn rằng mọi người có thể chỉ ra một vài sai sót:

template <typename T> 
class deferred_create_ptr : boost::noncopyable { 
private: 
    mutable T * m_pThing; 
    inline void createThingIfNeeded() const { if (!m_pThing) m_pThing = new T; } 
public: 
    inline deferred_create_ptr() : m_pThing(NULL) {} 
    inline ~deferred_create_ptr() { delete m_pThing; } 

    inline T * get() const { createThingIfNeeded(); return m_pThing; } 

    inline T & operator*() const { return *get(); } 
    inline T * operator->() const { return get(); } 

    // is this a good idea? unintended conversions? 
    inline T * operator T *() const { return get(); } 
}; 

Sử dụng type_traits có thể làm điều này tốt hơn ...

Bạn sẽ cần phiên bản khác nhau cho các con trỏ mảng, và bạn có thể phải chơi xung quanh một chút với một functor của người sáng tạo hoặc đối tượng nhà máy hoặc một cái gì đó nếu bạn muốn chuyển đối số cho hàm tạo của T.

Nhưng bạn có thể sử dụng nó như thế này:

class MyClass { 
public: 
    // don't need a constructor anymore, it comes up NULL automatically 
    QObject * getExpensiveObject() const { return expensive_object_; } 

protected: 
    deferred_create_ptr<QObject> expensive_object_; 
}; 

Thời gian để đi tắt và biên dịch này và xem nếu tôi có thể phá vỡ nó ... =)

+0

Có thể anh ta sẽ xem xét cả hai phiên bản của chúng ta, chỉ trong trường hợp hàm tạo mặc định không phải là những gì anh ta nghĩ, hoặc trong trường hợp null là một giá trị trả về hợp lệ từ CreateExpensiveObject. –

-1

getter của bạn không thực sự const từ nó thay đổi nội dung của đối tượng. Tôi nghĩ rằng bạn đang suy nghĩ về nó.

+1

Đây chính xác là những gì 'mutable' được phát minh - để bạn có thể khai báo một thứ' const' mặc dù bạn phải sửa đổi bộ nhớ của nó, mặc dù bạn biết rằng giao diện của nó sẽ luôn giống nhau. –

0

Đề xuất giải pháp thậm chí fancier here, nhưng nó không xử lý các loại không có hàm tạo mặc định ...

0

Tôi đã crated một lớp mẫu Lazy<T> với các tính năng sau:

  • giao diện quen thuộc tương tự như con trỏ thông minh tiêu chuẩn
  • Hỗ trợ các loại mà không constructor mặc định
  • Hỗ trợ (di chuyển) các loại mà không cần copy constructor
  • An toàn chủ đề
  • Có thể sao chép bằng ngữ nghĩa tham chiếu: Tất cả các bản sao có cùng trạng thái; giá trị của chúng chỉ được tạo một lần.

Đây là cách bạn sử dụng nó:

// Constructor takes function 
Lazy<Expensive> lazy([] { return Expensive(42); }); 

// Multiple ways to access value 
Expensive& a = *lazy; 
Expensive& b = lazy.value(); 
auto c = lazy->member; 

// Check if initialized 
if (lazy) { /* ... */ } 

Đây là việc thực hiện.

#pragma once 
#include <memory> 
#include <mutex> 

// Class template for lazy initialization. 
// Copies use reference semantics. 
template<typename T> 
class Lazy { 
    // Shared state between copies 
    struct State { 
     std::function<T()> createValue; 
     std::once_flag initialized; 
     std::unique_ptr<T> value; 
    }; 

public: 
    using value_type = T; 

    Lazy() = default; 

    explicit Lazy(std::function<T()> createValue) { 
     state->createValue = createValue; 
    } 

    explicit operator bool() const { 
     return static_cast<bool>(state->value); 
    } 

    T& value() { 
     init(); 
     return *state->value; 
    } 

    const T& value() const { 
     init(); 
     return *state->value; 
    } 

    T* operator->() { 
     return &value(); 
    } 

    const T* operator->() const { 
     return &value(); 
    } 

    T& operator*() { 
     return value(); 
    } 

    const T& operator*() const { 
     return value(); 
    } 

private: 
    void init() const { 
     std::call_once(state->initialized, [&] { state->value = std::make_unique<T>(state->createValue()); }); 
    } 

    std::shared_ptr<State> state = std::make_shared<State>(); 
}; 
+0

Đối với giấy phép MIT: Xem https://meta.stackexchange.com/questions/12527/do-i-have-to-worry-about-copyright-issues-for-code-posted-on-stack-overflow – Matthias

+0

@Matthias: Cảm ơn bạn đã cho tôi biết! Khi tôi bắt đầu trên StackOverflow, tôi khá chắc chắn rằng họ đã có một giấy phép mặc định hạn chế hơn. Quay lại sau đó, đặt mã của tôi theo Giấy phép MIT có ý nghĩa. Tôi vui vì bây giờ là CC, nên tôi đã xóa câu đó. –

0

Tôi đã chơi một chút với chủ đề này và đưa ra giải pháp thay thế trong trường hợp bạn sử dụng C++ 11. Hãy xem xét những điều sau:

class MyClass 
{ 
public: 
    MyClass() : 
     expensiveObjectLazyAccess() 
    { 
     // Set initial behavior to initialize the expensive object when called. 
     expensiveObjectLazyAccess = [this]() 
     { 
      // Consider wrapping result in a shared_ptr if this is the owner of the expensive object. 
      auto result = std::shared_ptr<ExpensiveType>(CreateExpensiveObject()); 

      // Maintain a local copy of the captured variable. 
      auto self = this; 

      // overwrite itself to a function which just returns the already initialized expensive object 
      // Note that all the captures of the lambda will be invalidated after this point, accessing them 
      // would result in undefined behavior. If the captured variables are needed after this they can be 
      // copied to local variable beforehand (i.e. self). 
      expensiveObjectLazyAccess = [result]() { return result.get(); }; 

      // Initialization is done, call self again. I'm calling self->GetExpensiveObject() just to 
      // illustrate that it's safe to call method on local copy of this. Using this->GetExpensiveObject() 
      // would be undefined behavior since the reassignment above destroys the lambda captured 
      // variables. Alternatively I could just use: 
      // return result.get(); 
      return self->GetExpensiveObject(); 
     }; 
    } 

    ExpensiveType* GetExpensiveObject() const 
    { 
     // Forward call to member function 
     return expensiveObjectLazyAccess(); 
    } 
private: 
    // hold a function returning the value instead of the value itself 
    std::function<ExpensiveType*()> expensiveObjectLazyAccess; 
}; 

Ý tưởng chính là giữ chức năng trả về đối tượng đắt tiền làm thành viên thay vì đối tượng. Trong constructor khởi tạo với một chức năng nào sau đây:

  • Khởi đối tượng đắt
  • Thay thế bản thân với một chức năng mà chụp các đối tượng đã khởi tạo và chỉ trả về nó.
  • Trả về đối tượng.

Điều tôi thích về điều này là mã khởi tạo vẫn được viết trong hàm tạo (nơi tôi tự nhiên đặt nó nếu không cần thiết) mặc dù nó sẽ chỉ được thực hiện khi truy vấn đầu tiên đối tượng xảy ra.

Một nhược điểm của phương pháp này là hàm std :: gán lại chính nó trong quá trình thực thi. Truy cập bất kỳ thành viên không tĩnh nào (bắt giữ trong trường hợp sử dụng lambda) sau khi phân công lại sẽ dẫn đến hành vi không xác định, vì vậy điều này đòi hỏi sự chú ý nhiều hơn. Đây cũng là loại hack vì GetExpensiveObject() là const nhưng nó vẫn thay đổi thuộc tính thành viên trong lần gọi đầu tiên.

Trong mã sản xuất, tôi có thể muốn chỉ làm cho thành viên có thể biến đổi thành James Curran được mô tả. Bằng cách này, API công khai của lớp bạn nêu rõ rằng thành viên không được coi là một phần của trạng thái đối tượng, do đó nó không ảnh hưởng đến constness.

Sau khi suy nghĩ thêm một chút, tôi thấy rằng std :: async với std :: launch :: deferred cũng có thể được sử dụng kết hợp với std :: shared_future để có thể truy xuất kết quả nhiều lần. Đây là mã số:

class MyClass 
{ 
public: 
    MyClass() : 
     deferredObj() 
    { 
     deferredObj = std::async(std::launch::deferred, []() 
     { 
      return std::shared_ptr<ExpensiveType>(CreateExpensiveObject()); 
     }); 
    } 

    const ExpensiveType* GetExpensiveObject() const 
    { 
     return deferredObj.get().get(); 
    } 
private: 
    std::shared_future<std::shared_ptr<ExpensiveType>> deferredObj; 
}; 
Các vấn đề liên quan