Açık şablon somutlaştırması - ne zaman kullanılır?


95

Birkaç hafta aradan sonra, Şablonlar hakkındaki bilgimi David Vandevoorde ve Nicolai M. Josuttis'in hazırladığı Templates - The Complete Guide kitabıyla genişletmeye ve genişletmeye çalışıyorum ve şu anda anlamaya çalıştığım şey, şablonların açık bir şekilde somutlaştırılmasıdır. .

Aslında mekanizma ile ilgili bir problemim yok, ancak bu özelliği kullanmak istediğim veya kullanmak istediğim bir durum hayal edemiyorum. Bunu bana açıklayabilecek biri varsa minnettar olmaktan daha fazlasını yapacağım.

Yanıtlar:


67

Doğrudan https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation adresinden kopyalandı :

Kodunuzda gerçekten kullanmadan şablonlu bir sınıf veya işlevin somut örneğini oluşturmak için açık somutlaştırmayı kullanabilirsiniz. Çünkü sen kitaplığı (.lib) oluşturarak kullanan şablonları dosyaları zaman bu yararlıdır dağıtımı için, uninstantiated şablon tanımları nesnenin (obj) dosyalarına koymak değildir.

(Örneğin, libstdc ++ açık örnekleme içeren std::basic_string<char,char_traits<char>,allocator<char> >(ki std::string) sen fonksiyonlarını kullanmak her zaman bu yüzden std::string, aynı işlev kodu nesnelere kopyalanması gerekmez. Sadece (bağlantı) libstdc için bu ++ başvurmak gerekir derleyici.)


8
Evet, MSVC CRT kitaplıkları, char ve wchar_t için özelleşmiş tüm akış, yerel ayar ve dize sınıfları için açık örneklere sahiptir. Ortaya çıkan .lib 5 megabayttan fazladır.
Hans Passant

4
Derleyici, şablonun açıkça başka bir yerde somutlaştırıldığını nasıl biliyor? Mevcut olduğu için sadece sınıf tanımını oluşturmayacak mı?

@STing: Eğer şablon somutlaştırılmışsa, sembol tablosunda bu fonksiyonların bir girişi olacaktır.
kennytm

@Kenny: Aynı derslikte zaten örneklenmişse mi demek istiyorsun? Herhangi bir derleyicinin aynı uzmanlığı aynı TU'da birden fazla kez somutlaştırmayacak kadar akıllı olduğunu varsayardım. Açık örneklemenin faydasının (derleme / bağlantı süreleri ile ilgili olarak) bir TU'da (açık bir şekilde) bir uzmanlaşmanın somutlaştırılması durumunda, kullanıldığı diğer TU'larda somutlaştırılmayacağını düşündüm. Hayır?

4
@Kenny: Örtük başlatmayı önlemek için GCC seçeneğini biliyorum, ancak bu bir standart değil. Bildiğim kadarıyla VC ++ 'nın böyle bir seçeneği yok. Açık örnek her zaman derleme / bağlantı sürelerinin iyileştirilmesi olarak lanse edilir (Bjarne tarafından bile), ancak bu amaca hizmet etmesi için, derleyicinin bir şekilde şablonları örtük olarak başlatmamasını (örneğin, GCC bayrağı aracılığıyla) bilmesi veya verilmemelidir. şablon tanımı, yalnızca bir bildirim. Bu kulağa doğru geliyor mu? Ben sadece birinin neden açık somutlaştırmanın kullanıldığını anlamaya çalışıyorum (somut türleri sınırlamak dışında).

86

Yalnızca birkaç açık tür için çalışmak istediğiniz bir şablon sınıfı tanımlarsanız.

Şablon bildirimini normal bir sınıf gibi üstbilgi dosyasına koyun.

Şablon tanımını normal bir sınıf gibi bir kaynak dosyaya koyun.

Ardından, kaynak dosyanın sonunda, yalnızca kullanılabilir olmasını istediğiniz sürümü açıkça somutlaştırın.

Aptalca örnek:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

Kaynak:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Ana

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

1
O derleyici belirli bir çeviri birimi (işlev tanımları dahil) tüm şablon tanımı varsa, bunu söylemek için doğru mu olacak (ne olursa olsun o uzmanlaşma olup olmamasından bağımsız olarak gerektiğinde şablonun bir uzmanlık örneğini açıkça başka TU örneği)? Yani, açık somutlaştırmanın derleme / bağlantı zamanı avantajlarından yararlanmak için, yalnızca şablon bildirimini dahil etmek gerekir, böylece derleyici onu başlatamaz ?

1
@ user123456: Muhtemelen derleyiciye bağlıdır. Ancak çoğu durumda büyük olasılıkla doğrudur.
Martin York

1
Derleyicinin önceden belirttiğiniz türler için bu açıkça başlatılmış sürümü kullanmasını sağlamanın bir yolu var mı, ancak daha sonra şablonu "tuhaf / beklenmedik" bir türle başlatmaya çalışırsanız, "normal" olarak çalışmasını sağlayın; şablonu gerektiği gibi somutlaştırıyor mu?
David Doria

2
açık örneklemlerin gerçekten kullanıldığından emin olmak için iyi bir kontrol / test ne olurdu? Yani işe yarıyor, ancak bunun sadece talep üzerine tüm şablonları başlatmadığına tam olarak ikna olmadım.
David Doria

7
Yukarıdaki yorum gevezeliklerinin çoğu artık doğru değil çünkü c ++ 11: Açık bir örnekleme bildirimi (bir harici şablon) örtük somutlamaları önler: aksi takdirde örtük bir somutlaştırmaya neden olacak kod, içinde başka bir yerde sağlanan açık somutlaştırma tanımını kullanmak zorundadır. program (tipik olarak başka bir dosyada: bu, derleme sürelerini azaltmak için kullanılabilir) en.cppreference.com/w/cpp/language/class_template
xaxxon

21

Açık örnekleme, derleme sürelerini ve nesne boyutlarını azaltmaya izin verir

Bunlar sağlayabileceği en büyük kazanımlar. Aşağıdaki bölümlerde ayrıntılı olarak açıklanan aşağıdaki iki etkiden gelirler:

  • Derleme araçlarının dahil olanları yeniden oluşturmasını önlemek için başlıklardan tanımları kaldırın
  • nesne yeniden tanımlama

Tanımları başlıklardan kaldır

Açık örnekleme, tanımları .cpp dosyasında bırakmanıza izin verir.

Tanım başlık üzerindeyken ve onu değiştirdiğinizde, akıllı bir yapı sistemi, düzinelerce dosya olabilecek tüm dosyaları yeniden derleyerek derlemeyi dayanılmaz derecede yavaşlatır.

Tanımları .cpp dosyalarına koymak, harici kitaplıkların şablonu kendi yeni sınıflarıyla yeniden kullanamaması gibi bir dezavantaja sahiptir, ancak aşağıdaki "Dahil edilen başlıklardan tanımları kaldırın, ancak şablonları bir harici API'yi de kullanıma sunun" aşağıda bir geçici çözüm göstermektedir.

Aşağıdaki somut örneklere bakın.

Nesneyi yeniden tanımlama kazanımları: sorunu anlamak

Bir başlık dosyasında bir şablonu tamamen tanımlarsanız, bu başlığı içeren her bir derleme birimi, yapılan her farklı şablon argümanı kullanımı için şablonun kendi örtük kopyasını derler.

Bu, çok fazla gereksiz disk kullanımı ve derleme zamanı anlamına gelir.

İşte bu dosyalardaki kullanımı nedeniyle hem main.cppve hem de notmain.cppörtük olarak tanımladığı somut bir örnek MyTemplate<int>.

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub yukarı akış .

Sembolleri şununla derleyin ve görüntüleyin nm:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

Çıktı:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Buradan man nm, bunun Wbir şablon işlevi olduğu için GCC'nin seçtiği zayıf sembol anlamına geldiğini görüyoruz . Zayıf sembol, için derlenen örtük olarak üretilen kodun MyTemplate<int>her iki dosyada da derlendiği anlamına gelir .

Birden çok tanımla bağlantı anında patlamamasının nedeni , bağlayıcının birden çok zayıf tanımı kabul etmesi ve son çalıştırılabilir dosyaya koymak için bunlardan birini seçmesidir.

Çıktıdaki sayıların anlamı:

  • 0000000000000000: bölüm içindeki adres. Bu sıfır, şablonların otomatik olarak kendi bölümlerine yerleştirilmesidir.
  • 0000000000000017: onlar için oluşturulan kodun boyutu

Bunu biraz daha net görebiliriz:

objdump -S main.o | c++filt

biten:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

ve çözülmemeye karar veren _ZN10MyTemplateIiE1fEiezilmiş adıdır .MyTemplate<int>::f(int)>c++filt

Böylece, her bir yöntem somutlaştırması için ayrı bir bölümün oluşturulduğunu ve her birinin tabii ki nesne dosyalarında yer kapladığını görüyoruz.

Nesneyi yeniden tanımlama problemine çözümler

Bu sorun, açık örnekleme kullanılarak ve aşağıdakilerden biri kullanılarak önlenebilir:

  • tanımlamayı hpp üzerinde tutun ve extern templateaçıkça başlatılacak türler için hpp'yi ekleyin .

    Açıklandığı gibi: extern şablonunun (C ++ 11) kullanılması extern template , tamamen tanımlanmış bir şablonun, açık somutlaştırmamız dışında derleme birimleri tarafından somutlaştırılmasını engeller. Bu şekilde, nihai nesnelerde yalnızca açık somutlaştırmamız tanımlanacaktır:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Dezavantaj:

    • Yalnızca başlık kitaplığıysanız, harici projeleri kendi açık örneğini yapmaya zorlarsınız. Yalnızca başlık içeren bir kitaplık değilseniz, bu çözüm muhtemelen en iyisidir.
    • şablon türü kendi projenizde tanımlanmışsa ve yerleşik bir benzeri değilse int, bunun için dahil etmeyi başlığa eklemeye zorlanmışsınız gibi görünür, ileriye dönük bir bildirim yeterli değildir: extern şablonu ve eksik türler Bu, başlık bağımlılıklarını artırır biraz.
  • tanımı cpp dosyasında taşımak, sadece bildirimi hpp üzerinde bırakın, yani orijinal örneği şu şekilde değiştirin:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    Dezavantajı: harici projeler şablonunuzu kendi türleriyle kullanamaz. Ayrıca tüm türleri açık bir şekilde somutlaştırmak zorunda kalırsınız. Ama belki de bu, programcıların unutmayacağı zamandan beri bir artıdır.

  • tanımlamayı hpp'de tutun ve extern templateher dahil edilene ekleyin :

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Dezavantajı: tüm dahil olanlar externCPP dosyalarına eklemek zorundadır , bu da programcıların yapmayı unutması muhtemeldir.

Bu çözümlerden herhangi biri ile nmartık şunları içerir:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

bu yüzden sadece istendiği gibi mytemplate.obir derlemeye sahip olduğunu görüyoruz MyTemplate<int>, while notmain.ove main.odeğil çünkü Utanımsız anlamına geliyor.

Tanımları dahil edilen başlıklardan kaldırın, ancak aynı zamanda şablonları yalnızca başlık içeren bir kitaplıkta harici bir API ortaya çıkarın

Kitaplığınız yalnızca başlık değilse, extern templateyöntem çalışacaktır, çünkü projeleri kullanmak yalnızca açık şablon somutlaştırmasının nesnesini içerecek olan nesne dosyanıza bağlanacaktır.

Ancak, yalnızca başlık kitaplıkları için, ikisini birden istiyorsanız:

  • projenizin derlemesini hızlandırın
  • Başlıkları, başkalarının kullanması için harici bir kitaplık API'si olarak göster

o zaman aşağıdakilerden birini deneyebilirsiniz:

    • mytemplate.hpp: şablon tanımı
    • mytemplate_interface.hpp: şablon bildirimi yalnızca tanımlarla eşleşen, tanım mytemplate_interface.hppyok
    • mytemplate.cpp: dahil edin mytemplate.hppve açık anlık bildirimler yapın
    • main.cppve kod tabanındaki diğer her yerde: dahil etme mytemplate_interface.hpp, değilmytemplate.hpp
    • mytemplate.hpp: şablon tanımı
    • mytemplate_implementation.hpp: örneklenecek her sınıfı içerir mytemplate.hppve eklerextern
    • mytemplate.cpp: dahil edin mytemplate.hppve açık anlık bildirimler yapın
    • main.cppve kod tabanındaki diğer her yerde: dahil etme mytemplate_implementation.hpp, değilmytemplate.hpp

Veya daha da iyisi birden çok başlık için: klasörünüzün içinde bir intf/ implklasör oluşturun includes/ve mytemplate.hppher zaman ad olarak kullanın .

mytemplate_interface.hppYaklaşım şu şekildedir:

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

Derleyin ve çalıştırın:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

Çıktı:

2

Ubuntu 18.04'te test edilmiştir.

C ++ 20 modülleri

https://en.cppreference.com/w/cpp/language/modules

Bu özelliğin, kullanıma sunulduğunda en iyi kurulumu sağlayacağını düşünüyorum, ancak henüz GCC 9.2.1'imde mevcut olmadığı için henüz kontrol etmedim.

Hızlandırmayı / disk kaydetmeyi elde etmek için yine de açık örnekleme yapmanız gerekecek, ancak en azından "Dahil edilen başlıklardan tanımları kaldırın, ancak aynı zamanda şablonları yaklaşık 100 kez kopyalamayı gerektirmeyen harici bir API'yi de ortaya çıkarın" için mantıklı bir çözümümüz olacak.

Beklenen kullanım (açık belirtme olmadan, tam sözdiziminin nasıl olacağından emin değilsiniz, bkz: C ++ 20 modülleri ile şablon açık örnekleme nasıl kullanılır? )

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

ve sonra derlemeden bahsedilir https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

Dolayısıyla, bundan clang'ın şablon arayüzü + uygulamasını sihire çıkarabildiğini görüyoruz helloworld.pcm, ki bu da kaynağın bazı LLVM ara temsillerini içermelidir: C ++ modül sisteminde şablonlar nasıl işlenir? bu da şablon spesifikasyonunun gerçekleşmesine izin verir.

Şablon oluşturmadan çok şey kazanıp kazanmayacağını görmek için yapınızı hızlı bir şekilde nasıl analiz edebilirsiniz?

Öyleyse, karmaşık bir projeniz var ve şablon somutlaştırmanın, tam yeniden düzenleme yapmadan önemli kazançlar sağlayıp sağlamayacağına karar vermek mi istiyorsunuz?

Aşağıdaki analiz, aşağıdakilerden bazı fikirleri ödünç alarak, deney yaparken ilk önce yeniden düzenleme yapmak için karar vermenize veya en azından en umut verici nesneleri seçmenize yardımcı olabilir: C ++ nesne dosyam çok büyük

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

Rüya: bir şablon derleyici önbelleği

Bence nihai çözüm şununla inşa edebilirsek olurdu:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

ve daha sonra myfile.oönceden derlenmiş şablonları dosyalarda otomatik olarak yeniden kullanır.

Bu, ekstra CLI seçeneğini derleme sisteminize geçirmenin yanı sıra programcılar için 0 ekstra çaba anlamına gelir.

Açık şablon somutlaştırmanın ikincil bir avantajı: IDE'lerin şablon örneklemelerini listelemesine yardım edin

Eclipse gibi bazı IDE'lerin "kullanılan tüm şablon örneklerinin bir listesini" çözemediğini buldum.

Örneğin, şablonlu bir kodun içindeyseniz ve şablonun olası değerlerini bulmak istiyorsanız, kurucu kullanımlarını tek tek bulmanız ve olası türleri tek tek çıkarmanız gerekir.

Ancak Eclipse 2020-03'te, sınıf adında Tüm kullanımları bul (Ctrl + Alt + G) araması yaparak açıkça başlatılmış şablonları kolayca listeleyebilirim, bu da beni örneğin:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

to:

template class AnimalTemplate<Dog>;

İşte bir demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

IDE dışında kullanabileceğiniz başka bir guerrila tekniği nm -C, son çalıştırılabilir dosya üzerinde çalıştırmak ve şablon adını grep etmektir:

nm -C main.out | grep AnimalTemplate

bu, doğrudan Dogörneklerden biri olduğuna işaret ediyor :

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.