C++17, sınıf şablonu argüman çıkarımını (CTAD) tanıtarak, derleyicinin std::pair p(1, 2.0) gibi yapılandırıcı argümanlardan şablon argümanlarını çıkarmasına olanak tanıdı. Ancak, bu özellik kesinlikle yalnızca sınıf şablonlarının kendilerine sınırlıydı. Karmaşık tür ifadeleri için sözdizimsel şeker sağlayan takma ad şablonları (örn., template<class T> using Vec = std::vector<T, MyAlloc<T>>;), CTAD'den hariç tutuldu çünkü bunlar sınıf şablonu değildir; bunlar ayrı tür takma adlarıdır. C++20'den önce standart, takma ad şablonlarına çıkarım yolarını ilişkilendirecek bir mekanizma sunmamıştı ve geliştiriciler ya altta yatan karmaşık türü açığa çıkarmak ya da ayrıntılı fabrika işlevleri yazmak zorunda kalıyordu.
Bu sınırlama, bir soyutlama sızıntısı yarattı. Geliştiriciler, özel ayrıştırıcılar veya belirli konteyner yapılandırmaları gibi uygulama ayrıntılarını kapsüllemek için tür takma adları tanımladığında, bu takma adların kullanıcıları CTAD'yi kullanma yeteneğini kaybetti. Örneğin, template<class T> using RingBuffer = std::vector<T, PoolAllocator<T>>; ile RingBuffer buf(100); yazmak, derleyicinin takma adı kullanarak çağrıldığında T'yi yapılandırıcı argümanlarından çıkaramadığı için bir derleme hatası ile sonuçlanıyordu. Bu, ayrıntılı açık şablon argümanlarını (RingBuffer<int>) zorunlu kılarak takma adın avantajlarını ortadan kaldırıyordu ve tür çıkarımının kritik olduğu genel kodu karıştırıyordu.
C++20, takma ad şablonları için çıkarım kılavuzları tanıyarak bunu çözer. Geliştiriciler artık yapılandırıcı argümanlarını, tanıdık -> sözdizimi kullanarak takma adın şablon parametrelerine nasıl eşleştireceklerini açıkça belirtebilir. Örneğin, template<class T> RingBuffer(size_t, T) -> RingBuffer<T>; derleyiciye, boyut ve değer ile RingBuffer oluştururken T'yi değerden çıkarması ve takma adı buna göre oluşturması talimatını verir. Bu kılavuz, takma ad adını altta yatan sınıf şablonunun yapıcılarıyla köprü kurar ve soyutlama bariyerini korurken sıfır çalışma zamanı aşımı sağlar.
#include <vector> #include <cstddef> template<class T> struct PoolAllocator { using value_type = T; PoolAllocator() = default; template<class U> PoolAllocator(const PoolAllocator<U>&) {} T* allocate(std::size_t n) { return std::allocator<T>().allocate(n); } void deallocate(T* p, std::size_t n) { std::allocator<T>().deallocate(p, n); } }; template<class T> using RingBuffer = std::vector<T, PoolAllocator<T>>; // C++20 çıkarım kılavuzu template<class T> RingBuffer(size_t, const T&) -> RingBuffer<T>; int main() { // C++20: T int olarak çıkarılır, PoolAllocator<int> otomatik olarak kullanılır RingBuffer buffer(100, 0); // C++20'den önce, bu gerektiriyordu: // RingBuffer<int> buffer(100, 0); }
Bir finans teknolojisi firması, tüm ara iş parçacığı iletişim tamponları için özel bir kilitlenme-free bellek havuzu kullanan yüksek performanslı bir piyasa veri işleyici geliştirdi. Kod tabanını basitleştirmek için, template<class T> using MessageQueue = std::vector<T, LockFreePoolAllocator<T>>; tanımladılar. Niceliksel geliştiricilerin bu kuyrukları sık sık farklı mesaj türleri ile oluşturması gerekiyordu (örn., PriceUpdate, OrderEvent), ancak zorunlu şablon sözdizimi (MessageQueue<PriceUpdate> q(1024);) algoritmik mantığı karıştırdı ve hızlı hata ayıklama oturumları sırasında bilişsel yükü artırdı.
Kritik bir ticaret oturumunda, bir junior geliştirici yanlışlıkla bir MessageQueue'yu açıkça std::vector<PriceUpdate> yazarak varsayılan ayrıştırıcıyı kullanarak başlattı ve bu, kilitlenme-free havuzu atlayarak sessiz bellek tahsis çakışmasına neden oldu. Bu, sistem gecikmesini 400 mikro saniye kadar arttırdı—yüksek frekanslı ticarette sonsuz bir süre. Ekip, takma ad şablonu sözdiziminin ayrıntılı olmasının, geliştiricileri tamamen soyutlamayı atlamaya teşvik ettiğini fark etti.
Çözüm 1: Fabrika işlevi şablonları.
Ekip, template<class T> auto make_message_queue(size_t n) { return MessageQueue<T>(n); } uygulamayı düşündü. Bu, auto q = make_message_queue<PriceUpdate>(1024); yazmayı sağlayacaktı. Ancak, bu yaklaşım türün argümanlardan çıkarılamadığı (örn., varsayılan oluşturma) durumlarda açık şablon argümanlarını gerektiriyor, yeni işe alımları karıştıran bir paralel "oluşturma API'si" oluşturuyordu ve ayrıca ek aşırı yükler olmaksızın kıvrımlı başlatıcı listelerini ({1, 2, 3}) desteklemiyordu. Ayrıca, kuyruğun diğer yerlerde şablon çıkarımı için açık tür isimleri gerektiren bağlamlarda kullanılmasına izin vermiyordu.
Çözüm 2: Makro tabanlı tür takma adları.
#define MESSAGE_QUEUE(T) std::vector<T, LockFreePoolAllocator<T>> kullanma önerisi hızla reddedildi. Makrolar tür sistemini atlar, isim alanlarını yok sayar, IDE yeniden düzenleme araçlarını kırar ve daha sonra alt tür spesifikasyonunu engeller. Firmanın kodlama standartları, daha önceki isim çakışmaları ve belirsiz derleme hataları ile ilgili sorunlar nedeniyle tür tanımlamaları için makroları kesin olarak yasaklamıştı.
Çözüm 3: C++20 geçişi ve çıkarım kılavuzları.
Ekip, derleyici araç zincirlerini C++20'ye geçirip bir çıkarım kılavuzu eklemeye karar verdi: template<class T> MessageQueue(size_t, const T&) -> MessageQueue<T>;. Bu, geliştiricilerin MessageQueue queue(1024, PriceUpdate{}); yazmasına veya geçici nesneler için kopya eleme ile güvenmeleri sağladı, böylece derleyici T'yi çıkarabilecekti. Bu, soyutlamayı korudu, tür güvenliğini sürdürdü ve derleyici sürümünden başka çalışma zamanı aşımı veya API değişiklikleri gerektirmedi.
Çözüm 3 uygulandı. Çıkarım kılavuzu, temel altyapı başlığına eklendi. Geçiş sonrası kod incelemeleri, şablonla ilgili sözdizimi hatalarında %40'lık bir azalma gösterdi. Daha önce bahsedilen gecikme sorunu ortadan kalktı çünkü geliştiriciler tutarlı bir şekilde takma adı kullandı. Ayrıca, sonraki çeyrekte statik analiz araçları "ayrıştırıcı atlama" olgusunu tespit etmedi, bu da CTAD'nın yazılımsal rahatlığı ile mimari soyutlamayı başarıyla uyguladığını, performanstan ödün vermeden kanıtladı.
Takma ad şablonu aracılığıyla bir nesne oluşturduğumda neden altta yatan sınıf şablonunun çıkarım kılavuzu otomatik olarak uygulanmıyor (örn., std::vector)?
Cevap.
Takma ad şablonları, derleyicinin tür sisteminde ayrı şablon varlıklarıdır; sadece sözel ikameler değildir. RingBuffer buf(100, 0); yazdığınızda, derleyici RingBuffer'ı altta yatan türe (std::vector<T, PoolAllocator<T>>) yalnızca takma ad için T'yi çıkarmaya çalıştıktan sonra çözümler. C++17 ve C++20 CTAD arama kuralları, çıkarım kılavuzunun ilanatta kullanılan belirli şablon adıyla ilişkilendirilmesini gerektirdiğinden, altta yatan sınıf için kılavuzlar, RingBuffer için başlangıç çıkarım aşamasında dikkate alınmaz. Takma ad şablonu esasen bir "çıkarma sınırı" oluşturur; takma ad için açık bir kılavuz olmadan, derleyici, altta yatan sınıfın argümanları için mükemmel kılavuzları olsa bile, yapılandırıcı argümanlarından takma adın şablon parametrelerine eşleme yapma konusunda yetersizdir.
Takma ad şablonu için çıkarım kılavuzu, ayrıştırıcının sabitlendiği durumlarda, altta yatan sınıftan daha az şablon parametresi olduğunda nasıl başa çıkar?
Cevap.
Takma ad şablonu için çıkarım kılavuzu yalnızca takma adın kendi şablon parametrelerini çıkarmak zorundadır. template<class T> using AllocVec = std::vector<T, FixedAllocator>; gibi bir takma ad için, kılavuz template<class T> AllocVec(size_t, const T&) -> AllocVec<T>; T'yi argümanlardan çıkarır. Sabit FixedAllocator, takma ad tanımının bir parçasıdır ve T bilindiğinde otomatik olarak değiştirilir. Adayların kaçırdığı ana nokta, altta yatan sınıfın takma adında bulunmayan son şablon argümanlarının ya varsayılan olması ya da tamamen takma adın parametreleri tarafından belirlenmiş olması gerektiğidir. Çıkarma kılavuzu, argümanlardan takma adın parametrelerine bir yansıma olarak işlev görür, altta yatan sınıf argümanlarının tamamının bütünsel bir belirtimi değil.
CTAD, tür dönüşümleri gerçekleştiren takma ad şablonları ile çalışabilir mi, örneğin template<class T> using VecOfOptional = std::vector<std::optional<T>>;, ve hangi sınırlamalar vardır?
Cevap.
Evet, CTAD bu tür takma adlarla çalışabilir, ancak çıkarım kılavuzu tür dönüşümünü açıkça hesaba katmalıdır. Eğer template<class T> VecOfOptional(size_t, T) -> VecOfOptional<T>; sağlayarak VecOfOptional(size_t, int) oluşturursanız, T int olarak çıkarılır ve std::vector<std::optional<int>> elde edilir. Ancak, dönüşüm türüyle doğrudan eşleşmeyen yapılandırıcı argümanları olduğunda yaygın bir tuzak vardır. Örneğin, doğrudan bir std::optional<T> ile yapmak istiyorsanız, kılavuz bunu yansıtmalıdır: template<class T> VecOfOptional(std::optional<T>) -> VecOfOptional<T>;. Adaylar sıklıkla derleyicinin dönüşümleri otomatik olarak "açmayacağını" yanlış anlar; bunu yapmayacaktır. Çıkarma kılavuzu, bileşenlerin takma adın şablon parametrelerine nasıl eşlendiğini açıkça belirtmelidir, hatta bu parametreler altta yatan kurulum içinde diğer türler içinde sarılı olsa bile.