Bugünkü yazımda son iki ayın genel temalarına odaklanacağım. Bugün mesele ne zaman ve nerede std::launder C++ 17'den ve fark nedir? reinterpret_cast VEYA std::start_lifetime_as oluşur.
Duyurudan sonra devamını okuyun
Andreas Fertig, dünya çapında yüz yüze ve uzaktan kurslar sunan uzman bir C++ eğitmeni ve danışmanıdır. C++ standartları komitesinde yer almakta ve uluslararası konferanslarda düzenli olarak konuşmalar yapmaktadır. C++ Insights (https://cppinsights.io) ile C++ programcılarının C++'ı daha iyi anlamalarına yardımcı olan, uluslararası alanda tanınan bir araç geliştirdi.
Bugün öğrendiklerinizi uygulayabileceğiniz birçok alan var. Gömülü alanda std::launder genellikle kullanılır, ancak kitaplık kodu yazarken “geri dönüşüm” de gerçekleşir.
Sorunlar ortaya çıkabildiğinde
P0532R0 belgesindeki örneği kullanıyorum:
struct X {
const int n; // #A
double d;
};
X* p = new X{7, 8.8}; // #B
new(p) X{42, 9.9}; // #C
int i = p->n; // #D
auto d = p->d; // #E
Burada birbirine uyması gereken birkaç parça var. Lütfen şunu unutmayın struct X veri alanı n GİBİ const ilan etti.
Sırada kullanım var new #B'de bir nesne yaratılır ve ortaya çıkan işaretçi şu konumda bulunur: p kaydedildi. Şimdiye kadar, çok iyi.
Duyurudan sonra devamını okuyun
İlginç kısım #C'de konumlandırmayla başlıyornew. Bunu daha önce hiç yapmadıysanız C++ kodunuzu yıkayıp ütülemenize gerek kalmayabilir.
Konumlandırmanew Aynı zamanda kendi içinde de iyidir. Sorun daha sonra #D ve #E'de ortaya çıkıyor. Burada işaretçi değerlerinden bahsediyoruz p erişimi vardı. Ancak derleyici #B'den sonra içeriğin olduğunu varsayabilir. p değişmedi, bu kadar p kendisi hiçbir zaman nesnenin içeriğini değiştirmek için kullanılmamıştır. Daha da kötüsü: veri unsurlarından biri X VE const. Bu, optimize edicinin değerini doğru bir şekilde üstlenmesi için ücretsiz bir geçiştir. n inşaattan sonra asla değişmez.
Ama #C'de yaptığım tam olarak buydu. Birini değiştireceğim const-Değer! değerleri i VE d bilinmiyorlar. Bu tanımlanmamış bir davranıştır.
olmadan bile bu kolayca önlenebilir. std::launderişaretçiyi güncellemek ve derleyiciye değerlerin geriye doğru olduğunu söylemek p değişti:
X* p = new X{7, 8.8}; // #B
p = new(p) X{42, 9.9}; // #C
int i = p->n; // #D
auto d = p->d; // #E
Öyleyse soru şu: Neden bunu yapıp unutmuyorsunuz? std::launder? Mümkün olduğunda unut std::launder.
Ne yazık ki işaretçiyi güncellemenin değerli kaynakları feda edeceği durumlar vardır.
İşler daha karmaşık hale geldiğinde
Özel bir ayırıcı uyguladığımızı varsayalım:
template<size_t SIZE, size_t ALIGNMENT>
class Buffer {
alignas(ALIGNMENT) std::byte mBuffer[SIZE];
public:
template<typename T, typename... Ts>
T* Construct(Ts... vals)
{
new(mBuffer) T{std::forward<Ts>(vals)...};
return reinterpret_cast<T*>(mBuffer);
}
template<typename T>
[[nodiscard]] T* Get()
{
return reinterpret_cast<T*>(mBuffer);
}
};
Burada ayırıcı tarafından sağlanan iki işlevi görebilirsiniz: Construct VE Get. Kavramı Buffer saklanan verilerin türden bağımsız olmasıdır. Olası basitleştirilmiş bir kullanım şu şekilde görünebilir:
struct Point { // #A
int x;
int y;
};
struct Point3D {
int x;
int y;
int z;
};
std::array<Buffer<12, 8>, 2> storage{}; // #B
// #C
storage.at(0).Construct<Point>(2, 3);
storage.at(1).Construct<Point3D>(4, 5, 6);
// #D
storage.at(0).Get<Point>()->x = 7;
İki tür veri Point VE Point3D #A'da isteğe bağlı türlerin örnekleri verilmiştir. storage isteğe bağlı verileri depolayabilen yığın belleği anlamına gelir. Kullanıcılar bir dizinin arkasında ne tür verinin bulunduğunu bildiği sürece (ve bu tür çok büyük değilse), her şey saklanabilir.
#C dizisinin ilk elemanına bir oluşturuyorum Pointikinci öğeye yazarken öğe Point3D yaratmak. Nesneler aslında yalnızca daha sonra #D programında kullanılacaktır. Ancak hiç kimsenin iyi bir nedenden dolayı yeni inşa edilen nesneye bir işaretçi saklamaması. Böyle bir işaretçi hafıza gerektirir ve bu işaretçinin nereye işaret ettiğini zaten biliyorsunuzdur.
Soyut bir makine perspektifinden bakıldığında, yazım sisteminde delikler açtım. Derleyici haklı olarak verilerin arkasında olduğunu varsayabilir. Get Doğrudan yazma erişimi #D'yi görmediğiniz sürece bunu değiştirmeyin. Mevcut bir konumda yeni bir tane oluşturduğumda #E'deki aşağıdaki kodda bu tür erişim derleyici tarafından fark edilmez (veya bu tanımsız bir davranış olduğundan fark edilmeyebilir) Point-Nesne oluştur:
std::array<Buffer<12, 8>, 2> storage{}; // #B
// #C
storage.at(0).Construct<Point>(2, 3);
// #D
storage.at(0).Get<Point>()->x = 7;
// #E
storage.at(0).Construct<Point>(8, 9);
Elbette buradaki sorundan kurtulmanın en kolay yolu her birini kullanmaktır. Constructçağrı döndürülen işaretçiyi saklar. Bu durumda olduğu gibi bu mümkün değilse doğru yaklaşım işaretçiyi kullanmaktır. std::launder “yıkamak”. Bu özel araç, derleyici optimizasyonlarını ve varsayımlarını engelleyen bir sanallaştırma bariyeri görevi görür.
std::launder kurtarmaya
İşte nasıl çalışıyor? Buffer-Uygulamayı güvenli bir şekilde güncelleyin. Hatta her şeyden önce Construct: Orijinal uygulamam şuna benziyordu:
template<typename T, typename... Ts>
T* Construct(Ts... vals)
{
new(mBuffer) T{std::forward<Ts>(vals)...};
return reinterpret_cast<T*>(mBuffer);
}
Bu kodu onsuz da kullanabilirsiniz std::launder den kullandığınızdan emin olun new dönüş işaretçisi:
template<typename T, typename... Ts>
T* Construct(Ts... vals)
{
return new(mBuffer) T{std::forward<Ts>(vals)...};
}
Düzeltmeniz gereken ikinci özellik Getbu sefer ekliyorum std::launder:
template<typename T>
[[nodiscard]] T* Get()
{
return std::launder(reinterpret_cast<T*>(mBuffer));
}
Kod artık sizin olduğunuzu varsayıyor Construct senden önce ara Get sen ara. Bu mBuffer ile eşleşen geçerli bir nesne içeriyor Construct yaratıldı.
Özet
İle std::launder o bellek adresinde zaten var olan bir nesneye işaret eden bir işaretçiyi güncelleyebilirsiniz. Nesnenin ömrü bu noktada zaten başlamıştır.
(Ben)

Bir yanıt yazın