2012-11-05 53 views
8

我有一個接口使用pimpl成語,但接口需要是可重入的。然而,調用線程不需要知道鎖定。這是四個部分的問題,一部分是免費的C++ 11例子(這個例子包含了幾個類似FAQ的問題,我已經運行過這個問題:lockingpimplrvalue和C++ 11,其中答案在某種程度上可疑他們的質量)。C++ 11可重入類鎖定策略

在標題中,example.hpp:

#ifndef EXAMPLE_HPP 
#define EXAMPLE_HPP 

#include <memory> 
#include <string> 

#ifndef BOOST_THREAD_SHARED_MUTEX_HPP 
# include <boost/thread/shared_mutex.hpp> 
#endif 

namespace stackoverflow { 

class Example final { 
public: 
    typedef ::boost::shared_mutex shared_mtx_t; 
    typedef ::boost::shared_lock<shared_mtx_t> shared_lock_t; 
    typedef ::boost::unique_lock<shared_mtx_t> unique_lock_t; 

    Example(); 
    Example(const std::string& initial_foo); 

    ~Example(); 
    Example(const Example&) = delete;    // Prevent copying 
    Example& operator=(const Example&) = delete; // Prevent assignment 

    // Example getter method that supports rvalues 
    std::string foo() const; 

    // Example setter method using perfect forwarding & move semantics. Anything 
    // that's std::string-like will work as a parameter. 
    template<typename T> 
    bool foo_set(T&& new_val); 

    // Begin foo_set() variants required to deal with C types (e.g. char[], 
    // char*). The rest of the foo_set() methods here are *NOT* required under 
    // normal circumstances. 

    // Setup a specialization for const char[] that simply forwards along a 
    // std::string. This is preferred over having to explicitly instantiate a 
    // bunch of const char[N] templates or possibly std::decay a char[] to a 
    // char* (i.e. using a std::string as a container is a Good Thing(tm)). 
    // 
    // Also, without this, it is required to explicitly instantiate the required 
    // variants of const char[N] someplace. For example, in example.cpp: 
    // 
    // template bool Example::foo_set<const char(&)[6]>(char const (&)[6]); 
    // template bool Example::foo_set<const char(&)[7]>(char const (&)[7]); 
    // template bool Example::foo_set<const char(&)[8]>(char const (&)[8]); 
    // ... 
    // 
    // Eww. Best to just forward to wrap new_val in a std::string and proxy 
    // along the call to foo_set<std::string>(). 
    template<std::size_t N> 
    bool foo_set(const char (&new_val)[N]) { return foo_set(std::string(new_val, N)); } 

    // Inline function overloads to support null terminated char* && const 
    // char* arguments. If there's a way to reduce this duplication with 
    // templates, I'm all ears because I wasn't able to generate a templated 
    // versions that didn't conflict with foo_set<T&&>(). 
    bool foo_set(char* new_val)  { return foo_set(std::string(new_val)); } 
    bool foo_set(const char* new_val) { return foo_set(std::string(new_val)); } 

    // End of the foo_set() overloads. 

    // Example getter method for a POD data type 
    bool bar(const std::size_t len, char* dst) const; 
    std::size_t bar_capacity() const; 

    // Example setter that uses a unique lock to access foo() 
    bool bar_set(const std::size_t len, const char* src); 

    // Question #1: I can't find any harm in making Impl public because the 
    // definition is opaque. Making Impl public, however, greatly helps with 
    // implementing Example, which does have access to Example::Impl's 
    // interface. This is also preferre, IMO, over using friend. 
    class Impl; 

private: 
    mutable shared_mtx_t rw_mtx_; 
    std::unique_ptr<Impl> impl_; 
}; 

} // namespace stackoverflow 

#endif // EXAMPLE_HPP 

然後在執行:

#include "example.hpp" 

#include <algorithm> 
#include <cstring> 
#include <utility> 

namespace stackoverflow { 

class Example; 
class Example::Impl; 


#if !defined(_MSC_VER) || _MSC_VER > 1600 
// Congratulations!, you're using a compiler that isn't broken 

// Explicitly instantiate std::string variants 
template bool Example::foo_set<std::string>(std::string&& src); 
template bool Example::foo_set<std::string&>(std::string& src); 
template bool Example::foo_set<const std::string&>(const std::string& src); 

// The following isn't required because of the array Example::foo_set() 
// specialization, but I'm leaving it here for reference. 
// 
// template bool Example::foo_set<const char(&)[7]>(char const (&)[7]); 
#else 
// MSVC workaround: msvc_rage_hate() isn't ever called, but use it to 
// instantiate all of the required templates. 
namespace { 
    void msvc_rage_hate() { 
    Example e; 
    const std::string a_const_str("a"); 
    std::string a_str("b"); 
    e.foo_set(a_const_str); 
    e.foo_set(a_str); 
    e.foo_set("c"); 
    e.foo_set(std::string("d")); 
    } 
} // anon namespace 
#endif // _MSC_VER 



// Example Private Implementation 

class Example::Impl final { 
public: 
    // ctors && obj boilerplate 
    Impl(); 
    Impl(const std::string& init_foo); 
    ~Impl() = default; 
    Impl(const Impl&) = delete; 
    Impl& operator=(const Impl&) = delete; 

    // Use a template because we don't care which Lockable concept or LockType 
    // is being used, just so long as a lock is held. 
    template <typename LockType> 
    bool bar(LockType& lk, std::size_t len, char* dst) const; 

    template <typename LockType> 
    std::size_t bar_capacity(LockType& lk) const; 

    // bar_set() requires a unique lock 
    bool bar_set(unique_lock_t& lk, const std::size_t len, const char* src); 

    template <typename LockType> 
    std::string foo(LockType& lk) const; 

    template <typename T> 
    bool foo_set(unique_lock_t& lk, T&& src); 

private: 
    // Example datatype that supports rvalue references 
    std::string foo_; 

    // Example POD datatype that doesn't support rvalue 
    static const std::size_t bar_capacity_ = 16; 
    char bar_[bar_capacity_ + 1]; 
}; 

// Example delegating ctor 
Example::Impl::Impl() : Impl("default foo value") {} 

Example::Impl::Impl(const std::string& init_foo) : foo_{init_foo} { 
    std::memset(bar_, 99 /* ASCII 'c' */, bar_capacity_); 
    bar_[bar_capacity_] = '\0'; // null padding 
} 


template <typename LockType> 
bool 
Example::Impl::bar(LockType& lk, const std::size_t len, char* dst) const { 
    BOOST_ASSERT(lk.owns_lock()); 
    if (len != bar_capacity(lk)) 
    return false; 
    std::memcpy(dst, bar_, len); 

    return true; 
} 


template <typename LockType> 
std::size_t 
Example::Impl::bar_capacity(LockType& lk) const { 
    BOOST_ASSERT(lk.owns_lock()); 
    return Impl::bar_capacity_; 
} 


bool 
Example::Impl::bar_set(unique_lock_t &lk, const std::size_t len, const char* src) { 
    BOOST_ASSERT(lk.owns_lock()); 

    // Return false if len is bigger than bar_capacity or the values are 
    // identical 
    if (len > bar_capacity(lk) || foo(lk) == src) 
    return false; 

    // Copy src to bar_, a side effect of updating foo_ if they're different 
    std::memcpy(bar_, src, std::min(len, bar_capacity(lk))); 
    foo_set(lk, std::string(src, len)); 
    return true; 
} 


template <typename LockType> 
std::string 
Example::Impl::foo(LockType& lk) const { 
    BOOST_ASSERT(lk.owns_lock()); 
    return foo_; 
} 


template <typename T> 
bool 
Example::Impl::foo_set(unique_lock_t &lk, T&& src) { 
    BOOST_ASSERT(lk.owns_lock()); 
    if (foo_ == src) return false; 
    foo_ = std::move(src); 
    return true; 
} 


// Example Public Interface 

Example::Example() : impl_(new Impl{}) {} 
Example::Example(const std::string& init_foo) : impl_(new Impl{init_foo}) {} 
Example::~Example() = default; 

bool 
Example::bar(const std::size_t len, char* dst) const { 
    shared_lock_t lk(rw_mtx_); 
    return impl_->bar(lk, len , dst); 
} 

std::size_t 
Example::bar_capacity() const { 
    shared_lock_t lk(rw_mtx_); 
    return impl_->bar_capacity(lk); 
} 

bool 
Example::bar_set(const std::size_t len, const char* src) { 
    unique_lock_t lk(rw_mtx_); 
    return impl_->bar_set(lk, len, src); 
} 

std::string 
Example::foo() const { 
    shared_lock_t lk(rw_mtx_); 
    return impl_->foo(lk); 
} 

template<typename T> 
bool 
Example::foo_set(T&& src) { 
    unique_lock_t lk(rw_mtx_); 
    return impl_->foo_set(lk, std::forward<T>(src)); 
} 

} // namespace stackoverflow 

而且我的問題是:

  1. 有沒有更好的方式來處理鎖定在私人執行?
  2. 由於定義是不透明的,是否會讓Impl公開?
  3. 當使用clang的-O4啓用Link-Time Optimization時,鏈接器應該有可能繞過std::unique_ptr的解除引用開銷。有沒有人驗證過?
  4. 有沒有辦法撥打foo_set("asdf")並正確連接示例鏈接?我們在計算出const char[6]的正確顯式模板實例時遇到問題。現在我已經設置了一個模板專門化,創建一個std::string對象並代理對foo_set()的調用。所有事情都考慮到了,這似乎是最好的前進方向,但我想知道如何通過「asdf」和結果。

關於鎖定策略,我已經開發了對這個明顯的偏見有以下幾個原因:

  • 我可以改變了互斥體是一個獨特的互斥酌情
  • 通過設計默認地將Impl API來包含所需的鎖定,鎖定語義的編譯時間保證非常強大
  • 很難忘記鎖定某些東西(並且在發生這種情況時出現「簡單的API」錯誤,編譯器會再次捕獲一旦API已被修復)
  • 這是很難留下點什麼鎖定或創建一個死鎖由於RAII和具有默認地將Impl沒有參考互斥
  • 的模板使用不再需要從一個獨特的鎖定降級到共享鎖
  • 因爲這種鎖定策略涵蓋的代碼比實際需要的要多,所以它需要明確地努力將鎖從獨特的降級到共享,這處理了常見的情況,其中共享鎖的假設在進入獨特時需要重新測試鎖定區域
  • 錯誤修正或Impl API更改不需要重新編譯整個應用程序,因爲example.hpp的API是外部修復的。

我讀過ACE使用這種類型的鎖定策略爲好,但我歡迎ACE用戶或其他一些真實世界的批評重新:圍繞穿過鎖定爲接口的必要組成部分。

爲了完整起見,這裏有一個example_main.cpp供人們咀嚼。

#include <sysexits.h> 

#include <cassert> 
#include <iostream> 
#include <memory> 
#include <stdexcept> 

#include "example.hpp" 

int 
main(const int /*argc*/, const char** /*argv*/) { 
    using std::cout; 
    using std::endl; 
    using stackoverflow::Example; 

    { 
    Example e; 
    cout << "Example's foo w/ empty ctor arg: " << e.foo() << endl; 
    } 

    { 
    Example e("foo"); 
    cout << "Example's foo w/ ctor arg: " << e.foo() << endl; 
    } 

    try { 
    Example e; 
    { // Test assignment from std::string 
     std::string str("cccccccc"); 
     e.foo_set(str); 
     assert(e.foo() == "cccccccc"); // Value is the same 
     assert(str.empty());   // Stole the contents of a_str 
    } 
    { // Test assignment from a const std::string 
     const std::string const_str("bbbbbbb"); 
     e.foo_set(const_str); 
     assert(const_str == "bbbbbbb");    // Value is the same 
     assert(const_str.c_str() != e.foo().c_str()); // Made a copy 
    } 
    { 
     // Test a const char[7] and a temporary std::string 
     e.foo_set("foobar"); 
     e.foo_set(std::string("ddddd")); 
    } 
    { // Test char[7] 
     char buf[7] = {"foobar"}; 
     e.foo_set(buf); 
     assert(e.foo() == "foobar"); 
    } 
    { //// And a *char[] & const *char[] 
     // Use unique_ptr to automatically free buf 
     std::unique_ptr<char[]> buf(new char[7]); 
     std::memcpy(buf.get(), "foobar", 6); 
     buf[6] = '\0'; 
     e.foo_set(buf.get()); 
     const char* const_ptr = buf.get(); 
     e.foo_set(const_ptr); 
     assert(e.foo() == "foobar"); 
    } 

    cout << "Example's bar capacity: " << e.bar_capacity() << endl; 
    const std::size_t len = e.bar_capacity(); 

    std::unique_ptr<char[]> buf(new char[len +1]); 

    // Copy bar in to buf 
    if (!e.bar(len, buf.get())) 
     throw std::runtime_error("Unable to get bar"); 
    buf[len] = '\0'; // Null terminate the C string 
    cout << endl << "foo and bar (a.k.a.) have different values:" << endl; 
    cout << "Example's foo value: " << e.foo() << endl; 
    cout << "Example's bar value: " << buf.get() << endl; 

    // Set bar, which has a side effect of calling foo_set() 
    buf[0] = 'c'; buf[1] = buf[2] = '+'; buf[3] = '\0'; 
    if (!e.bar_set(sizeof("c++") - 1, buf.get())) 
     throw std::runtime_error("Unable to set bar"); 

    cout << endl << "foo and bar now have identical values but only one lock was acquired when setting:" << endl; 
    cout << "Example's foo value: " << e.foo() << endl; 
    cout << "Example's bar value: " << buf.get() << endl; 
    } catch (...) { 
    return EX_SOFTWARE; 
    } 

    return EX_OK; 
} 

,並建立說明使用C++11libc++

clang++ -O4 -std=c++11 -stdlib=libc++ -I/path/to/boost/include -o example.cpp.o -c example.cpp 
clang++ -O4 -std=c++11 -stdlib=libc++ -I/path/to/boost/include -o example_main.cpp.o -c example_main.cpp 
clang++ -O4 -stdlib=libc++ -o example example.cpp.o example_main.cpp.o /path/to/boost/lib/libboost_exception-mt.dylib /path/to/boost/lib/libboost_system-mt.dylib /path/to/boost/lib/libboost_thread-mt.dylib 

作爲一個小的獎金,我更新了這個例子,包括在foo_set()方法,使用右值引用完美轉發。雖然不夠完美,但花費的時間比我預期的要長,這正是鏈接時遇到的問題。這還包括C基本類型的適當超載,包括:char*,const char*,char[N]const char[N]

+6

如果您使互斥鎖變爲可變,則可以使您的方法爲常量。 – inf

+3

ACE是醜陋的恕我直言 - 我不會把它作爲參考 - 雖然這是一個膝蓋猛拉的反應... – Caribou

+0

額外的包括警衛BOOST_THREAD_SHARED_MUTEX_HPP不是必需的,可能不會給你任何東西。爲什麼'最後'?它防止了一些在測試過程中可能有用的技巧。 –

回答

1

對於問題1,我試圖做的一件事就是使用SFINAE來限制允許輸入的鎖類型爲LockTypeshared_lock_tunique_lock_t

即:

template <typename LockType> 
typename std::enable_if< 
    std::is_same< LockType, shared_lock_t > || std::is_same< LockType, unique_lock_t >, 
    size_t 
>::type 
bar_capacity(LockType& lk) const; 

...但是,確實會有點冗長。

這意味着傳入錯誤類型的鎖會給你一個「沒有匹配」的錯誤。另一種方法是將兩個不同的bar_capacity公開,其中shared_lock_tunique_lock_t公開,並且私有的bar_capacity公開他們採用模板LockType

書面,任何類型的一個.owns_lock()方法返回一個類型轉換爲bool是一個有效的參數有...

1

使用PIMPL方法,互斥體應該實施的一部分。當鎖開始時,這會讓你掌握。

順便說一句,爲什麼使用unique_lock時lock_guard將足夠?

我沒有看到任何使impl公開的優點。

std :: unique_ptr應該與大多數現代編譯器的指針一樣高效。不過沒有驗證。

我會轉發爲const char [N] foo_set不是

template<std::size_t N> 
    bool foo_set(const char (&new_val)[N]) { return foo_set(std::string(new_val, N)); } 

但像

template<std::size_t N> 
    bool foo_set(const char (&new_val)[N]) { return foo_set(N, new_val); } 

這避免了在頭文件中的字符串創建,並讓實現做相應處理即可。

+0

re:Impl公開。我**是這樣做的,因爲我在.cpp中使用了幫助函數,正在交給'Impl的'const'引用,作爲使用編譯器強制執行無異常代碼的一種方式。然而,這不再是必要的!如果你閱讀了這篇博文中的例子https://codeblurbs.wordpress.com/2012/12/06/lust/(我需要證明/編輯),使用Lambdas減輕了對公共Impl的需求,而我非常興奮使用lambdas來強化編譯器保證。 – Sean

+0

re:foo_set()中的轉發,請嘗試在示例中編譯它。我向Scott Meyers反饋了一個音符:正確地說這是一個完美的向char *',const char *','char []'和const char []轉發,並且解決方案是一個醜陋的洞如果你找出了一些東西(或者我錯過了某些明顯的東西),我就會全神貫注。 – Sean

+0

正如你所看到的,我的建議並不是完美的轉發,而是轉向一個可以管理特定情況的案例。 –