標籤:

為什麼C/C++相同內存布局的struct不能互相cast?

struct a {
int b;
};

struct c {
int b;
};

int main() {
a a_;
c c_ = reinterpret_cast&(a_);
return 0;
}

struct a和struct b的內存布局沒理由不一樣吧. 但我根本沒辦法用簡單的方法cast. 我唯一找到的解決方案是union, 這是最佳實踐嗎?


如果你兩個struct完全相同,那為什麼不typedef A B?

如果你定義成兩個struct是為了將來擴展,那麼現在寫a=b也是不合邏輯的

如果你希望寫成兩個便於擴展,但是A和B之間的確存在業務上的等價賦值關係,那麼直接實現以對方為參數的拷貝構造和operator=就好,C的話可以自己寫個cast函數


用指針轉

c c_ = *reinterpret_cast&(a);


想要轉,就去用tuple。

union也是ub,不能轉就是不能轉,你繞過了編譯器檢查,也不能轉


void f(struct a x, struct b y)

{

x.b = 0; // 常識和編譯器都認為這句不會改變y的值(這有利於做各種優化)。但是要滿足這個假設,不能存在『 x 和 y 是同一對象的引用』的可能

}

記得C語言的規定是只有之前定義了union,才允許不同類型的值表示同一個對象。C++是直接不允許。都說C++複雜,然而在這一點上,C語言比C++更複雜


像這樣的要求,要不就轉指針,要不就寫構造函數,直接轉當然不行。

做個比喻,一瓶可口可樂,一瓶百事可樂。別人要可口可樂,你把百事給他他肯定不接受。現在你可以把可口可樂的包裝貼到百事上(轉指針),也可以把百事倒進可口可樂的瓶子里(構造函數拷貝)再給他。

那麼,現在問題來了,為什麼不直接給他一瓶可口可樂呢(滑稽


標準[1]中說了 ξ8.2.10 :

11 A glvalue expression of type T1 can be cast to the type 「reference to T2」 if an expression of type 「pointer to T1」 can be explicitly converted to the type 「pointer to T2」 using a reinterpret_cast. The result refers to the same object as the source glvalue, but with the speci?ed type. [Note: That is, for lvalues, a reference cast reinterpret_cast&(x) has the same e?ect as the conversion *reinterpret_cast&(x) with the built-in and * operators (and similarly for reinterpret_cast&(x)). —end note] No temporary is created, no copy is made, and constructors (15.1) or conversion functions (15.3) are not called.

也就是你得寫成這樣 編譯器才不會報錯

c c_ = reinterpret_cast&(a_);

或者這樣:

c c_ = *reinterpret_cast&(a_);

然後,在 6.10 這一節,說到:

8 If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is unde?ned:56
—(8.1) the dynamic type of the object,
—(8.2) a cv-quali?ed version of the dynamic type of the object,
—(8.3) a type similar (as de?ned in 7.5) to the dynamic type of the object,
—(8.4) a type that is the signed or unsigned type corresponding to the dynamic type of the object,
—(8.5) a type that is the signed or unsigned type corresponding to a cv-quali?ed version of the dynamic type of the object,
—(8.6) an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
—(8.7) a type that is a (possibly cv-quali?ed) base class type of the dynamic type of the object,
—(8.8) a char, unsigned char, or std::byte type.

我覺得最接近的情況應該是 8.3 了,於是又找到 7.5 節,說到:

1 A cv-decomposition of a type T is a sequence of cvi and Pi such that T is
「cv0 P0 cv1 P1 ··· cvn?1 Pn?1 cvn U」 for n &> 0,
where each cvi is a set of cv-quali?ers (6.9.3), and each Pi is 「pointer to」 (11.3.1), 「pointer to member of class Ci of type」 (11.3.3), 「array of Ni」, or 「array of unknown bound of」 (11.3.4). If Pi designates an array, the cv-quali?ers cvi+1 on the element type are also taken as the cv-quali?ers cvi of the array. [Example: The type denoted by the type-id const int ** has two cv-decompositions, taking U as 「int」 and as 「pointer to const int」. —end example] The n-tuple of cv-quali?ers after the ?rst one in the longest cv-decomposition of T, that is, cv1,cv2,...,cvn, is called the cv-quali?cation signature of T.
2 Two types T1 and T2 are similar if they have cv-decompositions with the same n such that corresponding Pi components are the same and the types denoted by U are the same.

按照這個表述,這兩個 struct,是不算 similar 的。

所以也就是說,不符合 8 種情況中的任意一種,也就是說,這個轉換是UB的

.

為什麼知乎編輯器這麼難用啊,代碼塊用完之後不能點下一行選擇語言也選不了,連弄個鏈接也那麼困難

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf


如果我對你說的union猜的沒錯

union.a_ union.b_的轉換是ub (active僅一個)(common initial 的成員另外一個member是可讀的,這個是well-defined)

再說了,不同類型的東西為什麼要能轉,除了些mocking test 但這又不是正常需求


假如你想找銀行貸款。

銀行說了,貸款可以,但你得有擔保。

於是你偷偷拿來你哥的房產證,把你哥的名字塗了,在旁邊歪歪扭扭寫上你的大名——哎呀你看,他要不同意,這麼重要的東西能到我手裡?你看這戶口本,親兄弟,這能假了?

你覺得銀行應該不應該同意拿這個做擔保?

做事是需要有規矩的。不能想怎樣就怎樣。

如果你哥真同意拿他的房產為你擔保,你得讓他親自和銀行簽下協議。

同樣的,如果兩個struct內存布局相同,何不寫成一個?

甚至,兩個struct內存布局不完全相同,但B可以兼容A,你也可以讓B繼承自A啊。這樣你就不需要在定義B時重新把A的元素定義一遍了——編譯器會在你修改A時,自動修改B,以使其保持對修改過的A的兼容。

你甚至可以通過讓B、C都從A繼承,來構造兩個「首部兼容但其它部分不同」的新結構體。沒關係,編譯器可保證你的代碼絕不會在這方面出任何紕漏。

——相比之下,C風格的強制類型轉換允許你徹徹底底的「隨心所欲」;但編譯器不會給你任何保障。想像下,N年後,當你完全忘了這回事時(或者接手了別人的代碼),只是改了改A的定義或者改了下內存對齊方式,結果軟體莫名其妙出現了運行時崩潰問題,但是崩潰卻發生在一個只接受B的動態庫函數里……

——所以,哪怕是C,都寧可用「結構體包含結構體」的方式來聲明兼容數據結構(從而可以利用編譯器切實保證兼容性),而不允許「我知道它們兼容,所以我直接強制類型轉換」(這種做法是可行的,但搞出這種飛機的傢伙很可能激怒同事,甚至可能被逐出團隊)。

正確聲明A兼容B的寫法:

C++風格:

struct A {
//sth
};

struct B : public A {
//other
};

C風格:

struct common {
//sth
};

struct A {
common c;
//other
};

struct B {
common c;
//other
};


從需求入手!從需求入手!從需求入手!凡是遇到這類問題怎麼強調都不夠。一個難題背後隱藏的需求往往跟它表面上完全不同,將它找出也能從根本上簡化方案。

即使reinterpret_cast允許強轉,你仍然需要保證兩個類的內存布局永遠相同,在工程中這是個非常不可靠的前提。你設計的是兩個不同但是可以轉化(或抽取可轉化部分)的類。既然如此為什麼不直接告訴c++呢?

「不同」:兩個類是無關的類,或者同一基類的不同擴展類,或者模板類的不同特化,甚至是同一個模板類不同模板參數(phantom tag)。「可以轉化」: 兩個類有共同的數據,例如包含相同的成員或基類。

在最簡單的情況下,你需要的只是從一個值構造另一個值:

struct Foo
{
int x;
};

struct Bar
{
Foo foo;
Bar(Foo const foo) : foo(foo) {}
};

int main()
{
Foo foo;
Bar bar {foo};
}

但有時你需要批量處理這些類型的轉換,於是你可以設計一個模板類,它通過模板參數來獲得強類型,但通過explicit conversion operator互通:

enum Phantom { PHANTOM1, PHANTOM2 };

template&
struct Foo final
{
int x;

template&
explicit operator Foo& ()
{
return *reinterpret_cast&*&>(this);
}
};

int func(Foo& foo) { return foo.x; }

int main()
{
Foo& foo1 {34465};
return func(static_cast&&>(foo1));
}

但是,樓主你是不是想要兩個類完全解耦合才會想到用cast呢?也許這兩個類是在不同項目里定義的,互相又不能引用。是時候祭出終極武器了:指定成員轉化!藉助模板的威(mo)力(fa),我們可以定義出一個好用的按指定成員名拷貝的函數,並且它完全可以被優化掉:

//這是一個簡單的結構體,儲存兩個不同類的相同類型成員指針
template&
struct MemberMatcher
{
U T1::* ptr1;
U T2::* ptr2;
};

//在C++17之前必須用一個函數幫助推導模板類型,C++17後可以直接用類名
template&
constexpr MemberMatcher& matcher(U T1::* ptr1, U T2::* ptr2)
{
return {ptr1, ptr2};
}

//遞歸用成員指針的匹配來拷貝成員
template&
constexpr void do_convert(
const From from,
To to,
MemberMatcher& matcher,
MemberMatcher&... matchers)
{
to.*(matcher.ptr2) = from.*(matcher.ptr1);
do_convert(from, to, matchers...);
}

template&
constexpr void do_convert(const From from, To to) { /*什麼都不用做,結束遞歸*/ }

//給用戶直接用的介面
template&
constexpr To convert(From from, MemberMatcher&... matchers)
{
To result;
do_convert(from, result, matchers...);
return result;
}

////////////////////////////////////////////////////////////////////////////////
//用戶代碼
struct Foo
{
int x;
float fff;
};

struct Bar
{
int y;
float gggg;
};

int main()
{
Foo foo;
Bar bar = convert(
foo,
matcher(Foo::x, Bar::y),
matcher(Foo::fff, Bar::gggg)
);
}

這個函數仍然有一些限制,例如兩個成員的類型必須相同,它沒有加以判斷成員是否可以拷貝(const,禁止拷貝的類型,或者成員函數),還有對於數組類型沒有處理。這些就留給讀者來思考了。


在編譯階段,編譯器只認Type(類型)。編譯器不可能笨到把兩個內存布局一樣的對象識別為同一個類型。

內存布局只有到了運行時才有實際意義。


既然你用的reinterpret_cast,那默認你在寫cpp,而cpp里的對象不只是幾個變數簡單的aggregation,而是存在對象語義的,光憑相同的內存布局,怎麼就能允許你直接cast呢?那構造函數和析構函數怎麼辦?所以如果你執意要這麼做,那麼就必須要通過memcpy之類的dirty ways了。

如果從C的角度來看的話,那就只是語言設計的選擇了,你可以參考龍書類型系統那節,看看那兩種類型等價的方式,具體名詞有點不記得了


http://en.cppreference.com/w/cpp/language/data_members#Standard_layout

http://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing

看這裡,不要用 reinterpret_cast ,用 union 是正確的。

但注意專門用 read 這個詞,而不是 access ,暗示不能 write 。

http://en.cppreference.com/w/cpp/string/byte/memcpy

用 memcpy 也可以。


用 union 做 type punning 是最佳實踐。


reinterpret_cast 用於 primitive type 間轉換,比如指針。


補充一下UB:

Accessing inactive union member and undefined behavior?

https://stackoverflow.com/questions/13334703/implementation-of-c-cast

然而reinterpret_cast對char/unsigned char/std::byte_t(C++1z)是開洞的,也就是你可以轉位元組來搞

顯然不相關的兩個類型轉是不合理的


取指針然後強制轉換指針就行

不能直接轉結構體的原因可能是從語義上來說這個需要返回一個結構體的右值吧,那就需要存到臨時的內存里,但是對reinterpret來說不管是用memcpy還是用拷貝構造都不太對,所以只允許轉引用和指針


指針轉不就行了


用指針顯式轉


雖然這兩個struct聲明是一模一樣的,但是編譯器並不會認為它們存在任何聯繫。因為C/C++主要採用了Nominal type system, 兩個類如果想要發生關係,必須顯式聲明,如:`struct a: public b {}`。兩個不相關的類型肯定不給你輕易轉的。

For example, in C, two struct types with different names in the same translation unit are never considered compatible, even if they have identical field declarations.

當然,C++在模板裡面也使用到了Structural type system。

PS: 用Typescript/Flow就可以輕易地搞來搞去


不是這麼用的吧老哥...


推薦閱讀:

程序員應該將精力放在研究編程語言本身還是用編程語言創造好的軟體?
Google對C++的影響有多大?
為什麼 C 語言源程序最後一行要是一個空行?
如何以最小的改動盡量不改變已有代碼的情況下適應不斷變更的需求?
如果要改進C語言,您最希望添加哪些語言特性,移除哪些語言特性?

TAG:C編程語言 | C | CC |