C++模板 - 实例之间的通用操作和成员可见性



作为实践,我决定实现一个非完整的线程安全和泛型堆栈。 您可以在下面看到类模板的初始声明:

template <typename T>
class ThreadsafeStack {
public:
ThreadsafeStack() = default;
ThreadsafeStack(ThreadsafeStack<T> const&);
template <typename U>
ThreadsafeStack(ThreadsafeStack<U> const&);
~ThreadsafeStack() = default;
ThreadsafeStack<T>& operator=(ThreadsafeStack<T> const&);
template <typename U>
ThreadsafeStack<T>& operator=(ThreadsafeStack<U> const&);
void push(T new_value);
std::shared_ptr<T> pop();
void pop(T& value);
bool empty() const;
private:
std::stack<T> data;
mutable std::mutex m;
};

我的问题是关于通用复制操作 (为简单起见,我暂时省略了移动操作)。 例如,请参阅"普通"和通用赋值运算符的定义:

// "Normal"
template <typename T>
ThreadsafeStack<T>& ThreadsafeStack<T>::operator=(ThreadsafeStack<T> const& o)
{
if ((void*) this == (void*) &o) {
return *this;
}
std::lock(m, o.m);
std::lock_guard<std::mutex> lock1(m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(o.m, std::adopt_lock);
data = o.data;
return *this;
}
// "Generalized"
template <typename T>
template <typename U>
ThreadsafeStack<T>& ThreadsafeStack<T>::operator=(ThreadsafeStack<U> const& o)
{
if ((void*) this == (void*) &o) {
return *this;
}
// ... ?
return *this;
}

使用此代码,问题是ThreadsafeStack<U>的成员std::mutex m不是 从ThreadsafeStack<T>可见 - 所以我可以锁定m但不能锁定o.m(与"正常"情况不同)。

(请注意,我必须锁定要复制的数据结构的互斥锁,而不会中断 整个复制过程。我不认为通过元素锁定和释放会更有效 或逻辑正确。考虑我有一个由值 5、6、7 组成的堆栈;首先,我复制 5,但之后 另一个线程弹出 6,所以我复制的下一个元素是 7......

那么,您将如何解决上述问题?

我已经考虑了以下解决方案:

  • 公开互斥锁成员(最糟糕的想法,不想这样做)。(在我看来,使用成员函数返回对它们的引用是一个类似的糟糕的 OO 设计想法。
  • 第二个:创建一个返回完整基础数据副本的公共成员函数(请注意,这种方式只能执行一次锁定)。我认为更长但类似的解决方法可能是使用已经存在的界面。
  • 不存在任何语言提供,更自然的方式来做到这一点吗?(例如,一些特殊语法,表示特定模板的所有实例化出于此类目的都看到彼此的私有成员和受保护成员。

还尝试在该主题上做一些研究:

  • C++ 模板 - 完整指南作者还指出 在第 5 章中,由于这个问题,他们不得不使用公众 界面进行复制。尽管我觉得这是一本很棒的书, 不幸的是,它有点旧,所以可能会更有效 如今的解决方案。

  • 对于
  • 斯科特来说,这可能也是正确的(与某些项目有关)。 迈耶的有效C++:第45项提到"正常"和概括 复制构造函数/赋值运算符,但它不够深入 这个具体方向。

  • 阅读有关SO的类似问题,但没有找到如此具体的问题 与通用操作的连接。

(注意:threadsafe 堆栈示例的基础知识来自 Anthony Williams 的C++ Concurrency in Action

非常感谢您提前提供的任何帮助。

问候 佐尔特

ThreadSafeStack<U>成为ThreadSafeStack<T>的朋友:

template <typename T>
class ThreadsafeStack {
public:
template <typename U>
friend class ThreadSafeStack;
//...
};
template <typename T>
template <typename U>
ThreadsafeStack<T>& ThreadsafeStack<T>::operator=(ThreadsafeStack<U> const& o)
{
if ((void*) this == (void*) &o) {
return *this;
}
std::lock(m, o.m);
std::lock_guard<std::mutex> lock1(m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(o.m, std::adopt_lock);
//...
return *this;
}

现在ThreadSafeStack<T>可以访问ThreadSafeStack<U>的私人成员。 不过,您如何实际将std::stack<T>复制到std::stack<U>仍然悬而未决;没有复制构造函数或赋值运算符可以执行此操作。

如果您已经实现了转换构造函数(如类声明中的建议):

template <typename U>
ThreadsafeStack(ThreadsafeStack<U> const&);

你只需要实现声明为的assing运算符:

ThreadsafeStack<T>& operator=(ThreadsafeStack<T> o);

使用复制和交换习惯用法,让转换构造函数完成艰苦的工作

您是否考虑过将lock()unlock()添加到模板类中?

我之前做过非常相似的事情,不同之处在于锁定是调用者的责任。 不过,这可以帮助您解决访问问题。

我的代码背后的想法是找到一种简单的方法,以非侵入性的方式使任何结构/类线程安全。

导出lock()unlock()使您的模板类也与 std::lock_guard 兼容,这很好。

#include <vector>
#include <mutex>
#include <iostream>
template <typename T, typename _Mutex = std::mutex>
class Lockable : public T
{
public:
typedef _Mutex Mutex;
private:
Mutex mtx_;
public:    
Lockable() = default;
Lockable(T&& x) : T(std::move(x)), mtx_() {}
Lockable(Lockable&& x) = default;
Lockable(const T& t) : T(t), mtx_() {}
template<typename M>
Lockable(const Lockable<T,M>& x) : T(x), mtx_() { }
//  needs few common operators, =, ==, !=
T& operator = (const T& x) { return T::operator=(x); }
template<typename M>
Lockable& operator = (const Lockable<T,M>& x) 
{
T::operator=(static_cast<const T&>(x));
return *this;
} 
void lock() const   { const_cast<Mutex&>(mtx_).lock(); }
void unlock() const { const_cast<Mutex&>(mtx_).unlock(); }
};

int main()
{
using LV = Lockable<std::vector<int>>;
LV vec = std::vector<int>(10, 42);
// this doesn't work, unfortunately 
//Lockable<int> z{2};
std::lock_guard<LV> lock{ vec };
for (auto& x : vec)
{
std::cout << "x: " << x << 'n';
}
return 0;
}

我看到了这个问题的多种方法和答案,非常感谢您一直以来的时间。 由于它们在某种意义上都是部分的,但仍然有用,因此我冒昧地根据您的建议提供全面的答案。

这是另一个答案建议的升级类声明(带有模板其他实例的友元声明):

template <typename T>
class ThreadsafeStack {
public:
template <typename U>
friend class ThreadsafeStack;
//...
};

但是,广义赋值运算符的定义并不完整。这是我完成它的方式:

template <typename T>
template <typename U>
ThreadsafeStack<T>& ThreadsafeStack<T>::operator=(ThreadsafeStack<U> const& o)
{
if ((void*) this == (void*) &o) {
return *this;
}
std::lock(m, o.m);
std::lock_guard<std::mutex> lock1(m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(o.m, std::adopt_lock);
data = std::stack<T>{};  // Note that the interface of std::stack doesn't provide a clear() member function.
auto tmp = o.data;
while (!tmp.empty()) {
data.push((T) tmp.top());
tmp.pop();
}
return *this;
}

请注意,此实现类似于复制和交换习惯用法,但不幸的是, 由于潜在的不同类型,交换部分必须以更长的方式执行。 (当然,类型的检查和转换可以进一步改进,但对于实际问题不是必需的。

最后,为了完成,这里是其余成员(复制构造函数和"普通"赋值运算符)的实现:

template <typename T>
ThreadsafeStack<T>::ThreadsafeStack(ThreadsafeStack<T> const& o)
{
std::lock_guard<std::mutex> lock(o.m);
data = o.data;
}
template <typename T>
template <typename U>
ThreadsafeStack<T>::ThreadsafeStack(ThreadsafeStack<U> const& o)
{
std::lock_guard<std::mutex> lock(o.m);
data = std::stack<T>{};
auto tmp = o.data;
while (!tmp.empty()) {
data.push((T) tmp.top());
tmp.pop();
}
}
template <typename T>
ThreadsafeStack<T>& ThreadsafeStack<T>::operator=(ThreadsafeStack<T> const& o)
{
if ((void*) this == (void*) &o) {
return *this;
}
std::lock(m, o.m);
std::lock_guard<std::mutex> lock1(m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(o.m, std::adopt_lock);
data = o.data;
return *this;
}

相关内容

最新更新