这是一个关于我的问题的例子:
struct B {
B(B&&, int = (throw 0, 0)) noexcept {}
};
Run Code Online (Sandbox Code Playgroud)
我知道这是一段非常奇怪的代码。它只是用来说明问题。的移动构造函数B有一个noexcept说明符,而它有一个抛出异常的默认参数。
如果我使用noexcept运算符来测试移动构造函数,它将返回false. 但是,如果我提供第二个参数,它将返回“true”(在 GCC 和 Clang 上):
noexcept( B(std::declval<B>()) ); // false
noexcept( B(std::declval<B>(), 1) ); // true
Run Code Online (Sandbox Code Playgroud)
然后我添加了 class D,它继承自B但不提供移动构造函数。
struct D : public B { };
Run Code Online (Sandbox Code Playgroud)
我测试了类D:
noexcept( D(std::declval<D>()) ); // true
Run Code Online (Sandbox Code Playgroud)
我已经阅读了标准,我认为根据标准,noexcept( D(std::declval<D>()) )应该返回false.
现在我试着按照标准来分析结果。
noexcept运算符的结果是,true除非表达式可能抛出([except.spec])。
所以现在我们需要判断表达式是否B(std::declval<B>())是 …
简要描述;简介:
下面的代码是《C++ Concurrency in Action 第二版》中无锁队列的实现。队列采用链表作为底层数据结构,采用分割引用计数技术实现。
compare_exchange_strong让我困惑的是,代码中有多个使用acquire内存顺序,但我不明白为什么使用这个顺序。
例如,在对对象count的成员的所有访问中node(见下文),不存在有顺序的存储操作release,但有很多有顺序compare_exchange_strong的操作acquire。
有关此无锁队列的完整代码、我对代码的解释以及我的问题的详细描述,请阅读完整描述部分。
详细描述:
我正在阅读 Anthony Williams 所著的《C++ Concurrency in Action》第二版。本书介绍了如何使用分割引用计数技术来实现无锁队列。我首先根据我的理解简单解释一下代码是如何工作的,以帮助你快速阅读代码。稍后将给出该队列的完整实现。
该实现使用单链表来实现队列。队列保存了指向链表头节点和尾节点的指针,分别是代码中的head和,并指向一个虚拟节点。tailtail
当将元素推入队列时,我们需要将元素值放入 指向的虚拟节点中tail,然后在该节点后面添加一个虚拟节点,并让tail指向新的虚拟节点。
当从队列中弹出一个元素时,我们应该弹出 指向的元素head,然后设置head为head->next。当head和tail指向同一个节点(即哑节点)时,队列为空。
该代码使用引用计数来管理已删除节点的生命周期。与节点相关的引用计数分为两部分,外部引用计数和内部引用计数。外部计数加上内部计数就是该节点的完整引用计数值。外部计数存储在指向节点的指针中(即counted_node_ptr在代码中),而内部计数存储在节点对象内部(即count在代码中)。
为了防止一个线程所指向的对象在解引用指针之前被另一个线程删除,外部计数器必须首先递增以确保该节点不被删除。这是通过increase_external_count()代码完成的。
当外部引用不再引用某个节点时,其中存储的外部计数必须添加到该节点的内部计数中,这是由 完成的free_external_counter()。内部计数存储在count.internal_count节点对象中。并count.external_counters表示引用该节点的外部引用的数量。由于这些外部引用各自拥有自己的外部计数,因此必须将这些计数全部考虑在内。当且仅当内部计数为0并且外部计数器的数量也为0时,该节点才能被安全删除。
对于pop其中未成功指向正在弹出的节点的引用(通常是因为另一个线程已经弹出了该节点),这些引用将被简单地丢弃,但必须减去它们的引用计数。这是由 完成的release_ref()。该函数将节点的内部计数减一。每次引用计数递减,或者内部计数和外部计数合并时,都会检查是否满足删除节点的条件。
下面是书中无锁队列的完整实现:
template<typename T> …Run Code Online (Sandbox Code Playgroud) 我正在阅读“计算机系统:程序员的视角,3/E”(CS:APP3e),以下代码是书中的示例:
long call_proc() {
long x1 = 1;
int x2 = 2;
short x3 = 3;
char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
Run Code Online (Sandbox Code Playgroud)
书中给出了GCC生成的汇编代码:
long call_proc()
call_proc:
; Set up arguments to proc
subq $32, %rsp ; Allocate 32-byte stack frame
movq $1, 24(%rsp) ; Store 1 in &x1
movl $2, 20(%rsp) ; Store 2 in &x2
movw $3, 18(%rsp) ; Store 3 in &x3
movb $4, 17(%rsp) ; Store 4 in …Run Code Online (Sandbox Code Playgroud) 这是来自temp.deduct.partial的示例。
template<class... Args> void f(Args... args); // #1
template<class T1, class... Args> void f(T1 a1, Args... args); // #2
template<class T1, class T2> void f(T1 a1, T2 a2); // #3
f(); // calls #1
f(1, 2, 3); // calls #2
f(1, 2); // calls #3; non-variadic template #3 is more specialized
// than the variadic templates #1 and #2
Run Code Online (Sandbox Code Playgroud)
为什么f(1, 2, 3)叫#2?
在CWG1395之前很容易理解。#2不能推导出,#1因为Args是一个参数包而T1不是,所以#2 …
我已经在 Google 和 Stack Overflow 上搜索了一些答案,并且我知道编译器不能假设函数不会修改通过 const 引用传递的参数,因为这些函数可能会通过const_cast. 但是,当原始对象本身定义为 时,这样做是未定义的行为const。来自cpreference
通过非常量访问路径修改常量对象并通过非易失性左值引用易失性对象会导致未定义的行为。
对于以下代码
void fun(const int&);
int f1() {
const int i = 3;
fun(i);
return i;
}
static int bar(const int i) {
fun(i);
return i;
}
int f2() {
return bar(3);
}
Run Code Online (Sandbox Code Playgroud)
GCC 和 Clang 都能够优化函数f1()以直接返回3,因为编译器认为调用fun(i)不会修改 的值i,因为这样的操作会导致未定义的行为。然而,GCC 和 Clang 都无法对该函数应用相同的优化f2()。编译器仍然生成代码以从内存加载值i。f1()下面是 GCC生成的代码f2()。编译器资源管理器
f1():
subq $24, %rsp
leaq 12(%rsp), …Run Code Online (Sandbox Code Playgroud) 以下是 MSVC STL 中的部分代码std::any:
class any {
// ...
struct _Small_storage_t {
unsigned char _Data[_Any_small_space_size];
const _Any_small_RTTI* _RTTI;
};
static_assert(sizeof(_Small_storage_t) == _Any_trivial_space_size);
struct _Big_storage_t {
// Pad so that _Ptr and _RTTI might share _TypeData's cache line
unsigned char _Padding[_Any_small_space_size - sizeof(void*)];
void* _Ptr;
const _Any_big_RTTI* _RTTI;
};
static_assert(sizeof(_Big_storage_t) == _Any_trivial_space_size);
struct _Storage_t {
union {
unsigned char _TrivialData[_Any_trivial_space_size];
_Small_storage_t _SmallStorage;
_Big_storage_t _BigStorage;
};
uintptr_t _TypeData;
};
static_assert(sizeof(_Storage_t) == _Any_trivial_space_size + sizeof(void*));
static_assert(is_standard_layout_v<_Storage_t>);
union {
_Storage_t _Storage{}; …Run Code Online (Sandbox Code Playgroud) 考虑以下示例:
template <class T>
struct A {
[[no_unique_address]] T t;
int i;
};
struct B {
long l;
int i;
};
class C {
long l;
int i;
};
Run Code Online (Sandbox Code Playgroud)
GCC 和 Clang 都认为sizeof(A<B>)是24,sizeof(A<C>)而是16。编译器资源管理器
类模板A<T>将属性T作为其数据成员之一[[no_unique_address]]。B和之间的唯一区别C是B是 astruct且C是 a class。我不明白为什么A<B>,而且A<C>尺寸不一样。换句话说,为什么编译器将i类模板的成员嵌入A<T>到 的尾部填充中C而不是嵌入到 的尾部填充中B?
如果我修改B成员访问权限 …
我正在阅读《C++ Concurrency in Action》第二版。下面的代码来自清单 7.6。它pop()使用危险指针来实现堆栈。
std::shared_ptr<T> pop() {
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
node* old_head = head.load(); // #1
do {
node* temp;
do {
temp = old_head;
hp.store(old_head); // #2
old_head = head.load(); // #3
} while (old_head != temp); // #4
} while (old_head &&
!head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr);
// ...
}
Run Code Online (Sandbox Code Playgroud)
书中解释了内循环的作用:
您必须在
while循环中执行此操作,以确保node在读取旧head指针#1和设置危险指针#2之间没有删除。在此窗口期间,没有其他线程知道您正在访问该特定节点。幸运的是,如果旧head节点要被删除,head那么它本身一定已经发生了变化,因此您可以检查这一点并继续循环,直到您知道该head指针仍然具有与您设置危险指针相同的值#4。
根据 的实现,如果另一个线程在和之间pop删除了头节点,则将被修改为新节点。pop …
来自cppreference:
运算符的重载
->必须返回原始指针,或者返回一个对象(通过引用或通过值),该对象->又重载运算符。
不过,我测试了以下示例,它被 GCC、Clang 和 MSVC 接受:
struct A {
int operator->();
};
Run Code Online (Sandbox Code Playgroud)
根据 cppreference,返回类型int既不是指针也不是重载的类型operator->。
为什么这个例子是正确的?标准中哪里描述了这种情况?标准允许这样做的目的是什么?