2012-11-05 17 views
8

Mam interfejs używając pimpl idiomu, jednak interfejs musi być wklęsłego. Jednak wątki wywołujące nie muszą być świadome blokowania. To cztery część pytanie i jedna część bezinteresownie wymyślony C++ 11 przykład (przykład zawarte zająć kilka FAQ-jak pytania, które mam natknąć re: locking, pimpl, rvalue i C++ 11, gdzie odpowiedzi były nieco wątpliwa w ich jakość).C++ 11 reentrant strategia zamek klasa

W nagłówku, 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 

A następnie w realizacji:

#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 

I moje pytania są następujące:

  1. Czy istnieje lepszy sposób obsłużyć blokowanie w prywatnej implementacji?
  2. Czy istnieje jakakolwiek rzeczywista usterka podczas wprowadzania Impl public, ponieważ definicja jest nieprzejrzysta?
  3. Podczas korzystania z języka -O4 dla klangu, aby umożliwić Link-Time Optimization, linker powinien ominąć narzut dereferencji std::unique_ptr. Czy ktoś to zweryfikował?
  4. Czy istnieje sposób, aby wywołać foo_set("asdf") i mają prawidłowo przykładowy odnośnik? Mamy problemy z ustaleniem, jaka jest właściwa instancja jawnego szablonu dla const char[6]. Na razie skonfigurowałem specjalizację szablonów, która tworzy obiekt std::string i wywołuje wywołanie foo_set(). Biorąc wszystko pod uwagę, wydaje się, że to najlepsza droga naprzód, ale chciałbym wiedzieć, jak przekazać wynik "asdf" i std::decay.

Jeśli chodzi o strategię blokowania, mam opracowane oczywistą zachętę do tego z kilku powodów:

  • mogę zmienić się mutex być ekskluzywną muteksu gdzie stosowne
  • Przy projektowaniu Impl API to wymagane blokady, jest bardzo silny gwarancja kompilacji semantyki blokujących
  • trudno jest zapomnieć, aby zablokować coś (i „proste API” bug kiedy tak się stanie, ponownie kompilator złapie to po naprawieniu API)
  • Trudno jest zostawić coś zablokowane lub utwórz martwego blokadę powodu RAII i posiadające IMPL nie mają odniesienia do mutex
  • użycie szablonów usuwa potrzebę przejść z unikalnym zamkiem do udostępnionego zamka
  • Ponieważ strategia ta blokująca obejmuje więcej kodu niż faktycznie jest wymagane, wymaga zdecydowanych starań, aby obniżyć wartość zamek od unikalny wspólny, który obsługuje wszystkie nazbyt często scenarior których założenia wykonane ze wspólną potrzebą blokady należy ponownie przebadać przy wejściu wyjątkowy zablokowana w okolicy
  • poprawki lub zmiany API iMPL nie wymagają rekompilacji całej aplikacji od example.hpp API jest zewnętrznie stałe.

Czytałem, że ACE używa tego typu strategię blokowania, jak również, ale jestem mile widziane jakieś RealWorld krytyki ze strony użytkowników ACE lub innych re: przechodząc wokół zamka jako wymaganej części interfejsu.

Dla dobra kompletności, oto przykładowa strona dla osób, które mogą żuć.

#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; 
} 

i budować instrukcje korzystania C++11 i libc++:

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 

Jako mały bonus, zaktualizowałem ten przykład to doskonałe przekazywanie za pomocą odniesień rvalue w metodzie foo_set(). Chociaż nie było to idealne, trwało dłużej, niż się spodziewałem, aby uzyskać poprawność wystąpienia szablonu, co jest problemem podczas łączenia. Obejmuje to również odpowiednie przeciążenia dla podstawowych typów C, w tym: char*, const char*, char[N] i const char[N].

+6

Możesz uczynić swoje metody const, jeśli zrobisz mutex 'mutable'. – inf

+3

ACE jest ohydnym IMHO - nie wziąłbym tego za referencję - chociaż jest to trochę reakcja na kolana ... – Caribou

+0

Dodatkowy atak to BOOST_THREAD_SHARED_MUTEX_HPP nie jest wymagany i prawdopodobnie nic ci nie daje. Dlaczego 'final'? Zapobiega niektórym sztuczkom, które mogą być przydatne podczas testowania. –

Odpowiedz

1

Na pytanie 1, jedną rzeczą, którą chciałbym zrobić, to użyć SFINAE do ograniczenia typów blokad przekazywanych jako LockType dopuszczonych do shared_lock_t lub unique_lock_t.

Ie:

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; 

... ale to robi się trochę rozwlekły.

co oznacza, że ​​podanie niewłaściwego typu zamka daje błąd "nic nie pasuje". Innym podejściem byłoby posiadanie dwóch różnych , które są narażone na działanie i prywatnej bar_capacity, które pobierają szablon LockType.

Jak napisano, każdy rodzaj z .owns_lock() metody zwracającej typu cabrio do bool to ważny argument tam ...

1

Korzystanie z idiom Pimpl, mutex powinien być częścią realizacji. Umożliwi to opanowanie po uruchomieniu blokady.

BTW, dlaczego używać unique_lock, gdy lock_guard będzie wystarczający?

Nie widzę żadnej korzyści, aby zrobić publicznie.

std :: unique_ptr powinien być równie skuteczny jak wskaźnik dla większości współczesnych kompilatorów. Nie zweryfikowano jednak.

chciałbym przekazać const char [N] foo_set nie jako

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

ale jak

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

To pozwala uniknąć tworzenia ciąg w pliku nagłówka i pozwól realizację robić, co jest potrzebne.

+0

re: Impl jest publiczny. Ja ** robiłem to, ponieważ korzystałem z funkcji pomocniczych w .cpp otrzymywałem referencje 'const' do Impl jako sposób użycia kompilatora do wymuszenia kodu bez wyjątków. Nie jest to już jednak konieczne! Jeśli przeczytasz przykłady w tym poście na blogu https://codeblurbs.wordpress.com/2012/12/06/lust/ (potrzebuję korekty/edycji), użycie Lambdas zmniejsza potrzebę publicznego Impl i jestem bardzo podekscytowany wykorzystaniem lambdas do egzekwowania gwarancji kompilatora. – Sean

+0

re: przekazywanie w foo_set(), spróbuj skompilować je w przykładzie. Oddałem notatkę Scottowi Meyersowi: dokładnie to (idealne przekazywanie dla 'char *', 'const char *', 'char []' i 'const char []', a rozwiązanie jest brzydką dziurą w spec. Jeśli coś wymyślisz (lub coś przeoczyłem), to ja mam uszy. – Sean

+0

Jak widzisz, moja sugestia nie była idealna do przekazania, ale do sprawy, która może zarządzać konkretnym przypadkiem. –