根据 C++ 标准,将指向固定数组(例如T(*)[N]或T(&)[N])的指针或引用转换为指向相同类型和 CV 限定(例如T(*)[M]或T(&)[M])的较小固定数组的指针或引用是否合法?
基本上,对于T(无论布局类型)的所有实例化,这是否总是格式良好的:
void consume(T(&array)[2]);
void receive(T(&array)[6])
{
consume(reinterpret_cast<T(&)[2]>(array));
}
Run Code Online (Sandbox Code Playgroud)
我没有看到任何对这是有效转换的引用:
然而,似乎所有主要编译器都接受这一点并生成正确的代码,即使在使用T = std::string( compiler explorer)时进行了优化(如果它是未定义的行为,这证明不是很多)。
我的理解是,根据类型系统,这应该是非法的,因为T[2]从未真正创建过对象,这意味着对的引用T(&)[2]将是无效的。
我将这个问题标记为c++11,因为这是我对答案最感兴趣的版本,但我很想知道这个答案在较新版本中是否有所不同。
根据is_destructible(http://eel.is/c++draft/meta.unary.prop#lib:is_destructible)的定义,is_destructible_v<T>是true:
要么
T是引用类型,要么T是一个完整的对象类型,当表达式declval<U&>().~U()被视为未评估的操作数时,表达式是格式良好的,其中U是remove_all_extents_t<T>.
它为什么使用declval<U&>().~U()而不是declval<U>().~U()?
declval在https://cplusplus.github.io/LWG/issue2049中添加了措辞以解决定义与抽象类型的问题.也许作者认为它declval<U>有返回类型U所以它不适用于抽象类型?
请让我开始,我知道从构造函数/析构函数中调用虚函数是一种不好的做法。然而,这样做的行为,虽然它可能令人困惑或不是用户所期望的,但仍然是明确定义的。
struct Base
{
Base()
{
Foo();
}
virtual ~Base() = default;
virtual void Foo() const
{
std::cout << "Base" << std::endl;
}
};
struct Derived : public Base
{
virtual void Foo() const
{
std::cout << "Derived" << std::endl;
}
};
int main(int argc, char** argv)
{
Base base;
Derived derived;
return 0;
}
Output:
Base
Base
Run Code Online (Sandbox Code Playgroud)
现在,回到我真正的问题。如果用户从不同线程的构造函数中调用虚函数会发生什么。有竞争条件吗?它是未定义的吗?或者换句话说。编译器设置 vtable 是线程安全的吗?
例子:
struct Base
{
Base() :
future_(std::async(std::launch::async, [this] { Foo(); }))
{
}
virtual ~Base() = default;
virtual void …Run Code Online (Sandbox Code Playgroud) 关于这段代码:
#include <string>
int main()
{
union u {
u() { i = 0; }
~u() {}
int i;
std::string s1;
std::string s2;
} u;
new (&u) std::string{};
}
Run Code Online (Sandbox Code Playgroud)
对象可以包含其他对象,称为子对象.子对象可以是成员子对象([class.mem]),基类子对象([class.derived])或数组元素.不是任何其他对象的子对象的对象称为完整对象.如果在与成员子对象或数组元素e关联的存储中创建对象(可能在其生命周期内或可能不在其生命周期内),则创建的对象是包含对象的子对象,如果:
- 包含对象的e的生命周期已经开始,未结束,并且
- 新对象的存储正好覆盖与e关联的存储位置,并且
- 新对象与e 的类型相同(忽略cv-qualification).
不需要如何在与成员子对象关联的存储中创建对象.如果子对象是标准布局联合的成员或非联合类对象的第一个成员,则代码不必在address-of运算符的参数中指定子对象.在这种情况下,足以获取包含对象的地址以指定成员子对象的存储.
"不需要如何创建对象",除其他外,意味着赋予放置new的指针不必指向子对象.主要是因为没有对象可以指向(注意,[intro.object]/2不要求子对象存活).在std-discussion邮件列表中,有人问,给定一个x类型的对象,和struct A { unsigned char buf[1]; };之间是否有区别?而答案是否定的,在这两种情况下,将提供存储的.因为new (&x) A{}new (x.buf) A{}x.bufA{}
[intro.object]和[basic.life]中的措辞与指针所代表的存储地址有关,而不是指向它的对象.
[class.union]/1发誓«联盟类型对象的大多数非静态数据成员可以随时激活».
哪一个在上面的代码中变得活跃,s1或者s2 …
下面代码中的静态断言会触发吗?
#include <type_traits>
using namespace std;
struct S { int i; };
using CS = const S;
static_assert(is_same_v<decltype((CS::i)), decltype((S::i))>);
Run Code Online (Sandbox Code Playgroud)
Clang 和 MSVC 没问题,GCC 认为断言触发,因为表示的类型decltype((CS::i))是const int&,表示的类型decltype((S::i))是int&:https : //godbolt.org/z/5MqPX_。(UPD:正如评论中所披露的那样,static_assert(is_same_v<decltype(CS::i), decltype(S::i)>);所有三个编译器都接受了https://godbolt.org/z/V5y-tD,但我对保证这一点的措辞感兴趣)。
[basic.type.qualifier]/1 似乎只保证T和const T具有相同的对象表示和对齐方式,但不保证const T的成员与 的相应成员具有相同的 cv 限定T。
该死,如果采用 100% 迂腐模式:我什至不认为标准要求查找const T应找到名为 的成员N,如果T有这样的成员。const T可以c_N代替N并具有与T(假设c_N …
这是C++ 17形式的规则([basic.lval]/8),但它在其他标准中看起来很相似("Lvalue"而不是C++ 98中的"glvalue"):
8如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
(8.4) - 对应于对象动态类型的有符号或无符号类型
规则听起来像"你会有UB,除非你做X",但这并不意味着如果你做X,你就不会得到UB,正如人们所期望的那样!事实上,X是有条件的或无条件的UB,具体取决于标准的版本.
让我们看看以下代码:
int i = -1;
unsigned j = reinterpret_cast<unsigned&>(i);
Run Code Online (Sandbox Code Playgroud)
这段代码的行为是什么?
[expr.reinterpret.cast]/10(C++ 11中的/ 11)(重点是我的):
如果可以使用reinterpret_cast将"指向T1的指针"类型的表达式显式转换为"指向T2的指针"类型,则可以将类型T1的左值表达式强制转换为"对T2的引用".也就是说,引用转换reinterpret_cast(x)与使用内置&和*运算符的转换*reinterpret_cast(&x)具有相同的效果.结果是一个左值,它引用与源左值相同的对象,但具有不同的类型.
所以reinterpret_cast<unsigned&>(i)左值是指int对象i,但是带有usigned类型.初始化需要初始化表达式的值,这正式意味着左值到右值的转换应用于左值.
[conv.lval]/1:
可以将非函数非数组类型T 的左值转换为右值.如果T是不完整类型,则需要进行此转换的程序格式不正确.如果左值引用的对象不是类型T的对象,并且不是从T派生的类型的对象,或者如果对象未初始化,则需要此转换的程序具有未定义的行为.
我们的左值unsigned类型不是指类型的对象,unsigned这意味着行为是未定义的.
在这些标准中,情况稍微复杂一些,但规则略有放松.[expr.reinterpret.cast]/11告诉同样的事情:
结果引用与源glvalue相同的对象,但具有指定的类型.
关于UB的违规措辞已从[conv.lval]/1中删除:
可以将非函数非数组类型T 的glvalue 转换为prvalue.如果T是不完整类型,则需要进行此转换的程序格式不正确.如果T是非类类型,则prvalue的类型是T的非限定版本.否则,prvalue的类型是T.
但是L-to-R转换的读取值是多少?[conv.lval] /(2.6)(/(3.4)in C++ 17)回答了这个问题:
... glvalue指示的对象中包含的值是prvalue结果
unsignedlvalue reinterpret_cast<unsigned&>(i)表示i int具有该值的对象, …
c++ strict-aliasing language-lawyer reinterpret-cast type-punning
提出这个问题:
的C11标准规定,一个指向联合可以被转换为一个指针到它的每一个构件.从第6.7.2.1.1节开始:
联合的大小足以包含其最大的成员.最多一个成员的值可以随时存储在union对象中. 指向适当转换的union对象的指针指向其每个成员 (或者如果成员是位字段,则指向它所在的单位),反之亦然.
这意味着您可以执行以下操作:
union u {
int a;
double b;
};
union u myunion;
int *i = (int *)&u;
double *d = (double *)&u;
u.a = 2;
printf("*i=%d\n", *i);
u.b = 3.5;
printf("*d=%f\n", *d);
Run Code Online (Sandbox Code Playgroud)
但对于反向:在以上工会的情况下,可以在一个int *或double *安全地转换为union u *?请考虑以下代码:
#include <stdio.h>
union u {
int a;
double b;
};
void f(int isint, union u *p)
{
if (isint) {
printf("int value=%d\n", p->a);
} else {
printf("double value=%f\n", …Run Code Online (Sandbox Code Playgroud) 编译器在编译代码时出现分歧
int main()
{
constexpr int arr[3] = {};
static_assert((void*)(arr + 3) == (void*)(&arr + 1));
}
Run Code Online (Sandbox Code Playgroud)
在 GCC 和 Clang 中,static_assert不会触发,MSVC 认为static_assert失败https://godbolt.org/z/dHgmEN
应(void*)(arr + 3) == (void*)(&arr + 1)评估为true,为什么?
假设我有 2 个 TU,其中有 2 个非内联函数定义,带有外部链接,它们仅在返回类型上有所不同。
我的程序违反了哪些段落?
[basic.def.odr]/4说:
每个程序都应包含在该程序中被 odr 在丢弃语句之外使用的每个非内联函数或变量的一个定义;无需诊断。
但
我正在使用一个C库,它使用无符号整数作为某些数据的索引.但有时,函数会将这些索引返回为signed,以便-1在函数无法返回索引时返回.*
implicit conversion changes signedness如果无法进行转换,如何防止警告,而是抛出运行时错误?您是否建议将库函数包装为使用异常进行错误处理并仅返回正确的值?有没有标准的方法来做到这一点:
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
// pointless c function to demonstrate the question
// parse the string to an unsigned integer, return -1 on failure
int atoui(char const* str) {
char* pend;
long int li=strtol(str, &pend, 10);
if ( errno!=0 || *pend!='\0' || li<0 || li>INT_MAX ) {
return -1;
} else {
return li;
}
}
// --8<---
#include <stdexcept>
// How to do this properly?
unsigned int unsign(int i) …Run Code Online (Sandbox Code Playgroud) 以下代码使用(本机 64 位)g++-10 在 64 位 Linux 系统上编译无警告:
#include <cstdint>
class A {
public:
explicit A(unsigned long long int x) { };
explicit A(uint64_t x) { };
};
int main() {}
Run Code Online (Sandbox Code Playgroud)
但是,在 32 位系统上(相同的 Linux 发行版,相同的版本,但它是 32 位机器),我收到此错误:
t.cc:6:14: error: ‘A::A(uint64_t)’ cannot be overloaded with ‘A::A(long long unsigned int)’
6 | explicit A(uint64_t x) { };
| ^
t.cc:5:14: note: previous declaration ‘A::A(long long unsigned int)’
5 | explicit A(unsigned long long int x) { };
Run Code Online (Sandbox Code Playgroud)
我通过实验测试了位大小( printf-ing sizeof(long long …
让我们看看以下代码:
int i = 10;
char c = reinterpret_cast<char&>(i);
Run Code Online (Sandbox Code Playgroud)
如果可以使用reinterpret_cast将"指向T1的指针"类型的表达式显式转换为"指向T2的指针"类型,则可以将类型T1的glvalue表达式强制转换为"对T2的引用".结果引用与源glvalue相同的对象,但具有指定的类型.
所以reinterpret_cast<char&>(i)具有指定char类型的左值引用该int对象i.
要进行初始化c,我们需要值,因此应用左值到右值的转换[conv.lval] /3.4:
glvalue指示的对象中包含的值是prvalue结果.
L2R转换的结果是i对象中包含的值.只要值i在char([expr]/4表示否则这是UB)可表示的范围内,变量c应初始化为具有相同的值.
从实现POV开始,在little-endian平台上,通过读取i对象地址处的字节可以轻松实现.但是,在big-endian平台上,编译器必须添加一个偏移量来获取最低有效字节.或者,将整个int对象读入寄存器并屏蔽第一个字节,这对两个端点都是可接受的方法.
如果您认为编译器可以轻松处理上面的代码以生成符合C++ 17标准所要求的代码,那么可以考虑将指针int指向指向i的指针char.这样的转换不会改变指针值,即它仍然指向int对象i,这意味着将间接运算符应用于具有以下L2R转换的指针应该如上所述,即int如果是,则获取对象的值按char类型表示.
在以下代码中
int i = 10;
f(reinterpret_cast<char*>(&i)); // void f(char*)
Run Code Online (Sandbox Code Playgroud)
如果编译器i不知道函数f将对其参数做什么,那么编译器是否应该调整某个偏移的地址?并且编译器也不知道将传递给函数的内容f.上面的代码和函数f在不同的翻译单元中.
例如,如果f …
c++ ×11
language-lawyer ×10
unions ×2
32bit-64bit ×1
arrays ×1
byte ×1
c ×1
c++11 ×1
c++17 ×1
constants ×1
decltype ×1
endianness ×1
g++ ×1
lifetime ×1
pointers ×1
type-punning ×1
type-traits ×1