Şablonlu C ++ sınıflarını .hpp / .cpp dosyalarına bölmek - mümkün mü?


99

A .hppve .cppdosya arasında bölünmüş bir C ++ şablon sınıfını derlemeye çalışırken hatalar alıyorum :

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

İşte kodum:

stack.hpp :

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp :

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp :

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ldelbette doğru: semboller yok stack.o.

Zaten söylediği gibi yaptığım için bu sorunun cevabı yardımcı olmuyor.
Bu yardımcı olabilir, ancak her yöntemi tek tek .hppdosyaya taşımak istemiyorum — Yapmamalıyım , değil mi?

.cppDosyadaki her şeyi dosyaya taşımak .hppve bağımsız bir nesne dosyası olarak bağlantı kurmak yerine her şeyi dahil etmek için tek makul çözüm bu mu? Bu çok çirkin görünüyor ! Bu durumda, benim de önceki durumuna ve yeniden adlandırma geri olabilir stack.cppiçin stack.hppve onunla yapılabilir.


Kodunuzu gerçekten gizli tutmak (ikili bir dosyada) veya temiz tutmak istediğinizde iki harika geçici çözüm var. İlk durumda da olsa genelliği azaltmak gerekiyor. Burada açıklanmıştır: stackoverflow.com/questions/495021/…
Sheric

Açık şablon somutlaştırma, şablonların derleme süresini
azaltmanın yoludur

Yanıtlar:


152

Bir şablon sınıfının uygulamasını ayrı bir cpp dosyasına yazmak ve derlemek mümkün değildir. Bunu yapmanın tüm yolları, eğer herhangi biri iddia ederse, ayrı cpp dosyasının kullanımını taklit etmek için geçici çözümlerdir, ancak pratik olarak bir şablon sınıf kitaplığı yazmayı ve uygulamayı gizlemek için başlık ve lib dosyalarıyla dağıtmayı düşünüyorsanız, bu mümkün değildir. .

Nedenini öğrenmek için derleme sürecine bakalım. Başlık dosyaları asla derlenmez. Yalnızca ön işlemden geçirilmiştir. Önceden işlenmiş kod daha sonra gerçekten derlenen cpp dosyasıyla çemberlenir. Şimdi, derleyicinin nesne için uygun bellek düzenini oluşturması gerekiyorsa, şablon sınıfının veri türünü bilmesi gerekir.

Aslında, şablon sınıfının bir sınıf değil, açıklaması ve tanımı derleyici tarafından argümandan veri türünün bilgisini aldıktan sonra derleme zamanında üretilen bir sınıf için şablon olduğu anlaşılmalıdır. Bellek düzeni oluşturulamadığı sürece, yöntem tanımlaması için talimatlar oluşturulamaz. Sınıf yönteminin ilk argümanının 'this' operatörü olduğunu unutmayın. Tüm sınıf yöntemleri, üzerinde çalıştığı nesne olarak ad değiştirme ve ilk parametre ile ayrı yöntemlere dönüştürülür. 'Bu' argümanı, kullanıcı nesneyi geçerli bir tür bağımsız değişkeni ile başlatmadıkça, şablon sınıfının derleyici için kullanılamayacağı nesnenin boyutu hakkında bilgi verir. Bu durumda, yöntem tanımlarını ayrı bir cpp dosyasına koyarsanız ve onu derlemeye çalışırsanız, nesne dosyasının kendisi sınıf bilgileriyle oluşturulmayacaktır. Derleme başarısız olmaz, nesne dosyasını oluşturur, ancak nesne dosyasındaki şablon sınıfı için herhangi bir kod üretmez. Bağlayıcının nesne dosyalarındaki sembolleri bulamamasının ve yapının başarısız olmasının nedeni budur.

Şimdi, önemli uygulama ayrıntılarını gizlemenin alternatifi nedir? Hepimizin bildiği gibi, arayüzü uygulamadan ayırmanın arkasındaki temel amaç, uygulama ayrıntılarını ikili biçimde gizlemektir. Veri yapılarını ve algoritmaları ayırmanız gereken yer burasıdır. Şablon sınıflarınız, algoritmaları değil yalnızca veri yapılarını temsil etmelidir. Bu, şablon sınıfları üzerinde çalışan veya yalnızca verileri tutmak için bunları kullanan sınıflar olan, şablonlaştırılmamış ayrı sınıf kitaplıklarında daha değerli uygulama ayrıntılarını gizlemenizi sağlar. Şablon sınıfı aslında verileri atamak, almak ve ayarlamak için daha az kod içerir. İşin geri kalanı algoritma sınıfları tarafından yapılacaktır.

Umarım bu tartışma yardımcı olur.


2
"şablon sınıfın bir sınıf olmadığı anlaşılmalıdır" - tam tersi değil miydi? Sınıf şablonu bir şablondur. "Şablon sınıfı" bazen "şablonun somutlaştırılması" yerine kullanılır ve gerçek bir sınıf olacaktır.
Xupicor

Sadece referans için, geçici çözüm olmadığını söylemek doğru değil! Veri Yapılarını yöntemlerden ayırmak, kapsüllemenin aksine kötü bir fikirdir. Bazı durumlarda (çoğuna inanıyorum) kullanabileceğiniz harika bir çözüm var: stackoverflow.com/questions/495021/…
Sheric

@Xupicor, haklısın. Teknik olarak "Sınıf Şablonu", bir "Şablon Sınıfı" ve ona karşılık gelen nesneyi başlatabilmeniz için yazdığınız şeydir. Bununla birlikte, genel bir terminolojide, her iki terimi birbirinin yerine kullanmanın o kadar da yanlış olmayacağına inanıyorum, "Sınıf Şablonu" nu tanımlamak için sözdizimi "sınıf" değil "şablon" kelimesiyle başlar.
Sharjith N.

@Sheric, geçici çözüm olmadığını söylemedim. Aslında, mevcut olanların tümü, yalnızca şablon sınıfları durumunda arayüz ve uygulamanın ayrılmasını taklit eden geçici çözümlerdir. Bu geçici çözümlerin hiçbiri, belirli bir tür şablon sınıfının örneğini oluşturmadan çalışmaz. Bu yine de sınıf şablonlarını kullanmanın tüm genellik noktasını ortadan kaldırır. Veri yapılarını algoritmalardan ayırmak, veri yapılarını yöntemlerden ayırmakla aynı şey değildir. Veri yapısı sınıfları çok iyi yapıcılar, alıcılar ve ayarlayıcılar gibi yöntemlere sahip olabilir.
Sharjith N.

Bu çalışmayı yapmak için bulduğum en yakın şey, bir çift .h / .hpp dosyası kullanmak ve şablon sınıfınızı tanımlayan .h dosyasının sonuna #include "dosyaadı.hpp". (noktalı virgülle sınıf tanımı için kapanış ayracınızın altında). Bu, en azından yapısal olarak onları dosya biçiminde ayırır ve buna izin verilir, çünkü sonunda, derleyici .hpp kodunuzu #include "dosyaadı.hpp" dosyanızın üzerine kopyalayıp / yapıştırır.
Artorias2718

90

O ise size ihtiyacı için neler örneklemesi biliyoruz sürece, mümkün.

Stack.cpp'nin sonuna aşağıdaki kodu ekleyin ve çalışacaktır:

template class stack<int>;

Şablon olmayan tüm yığın yöntemleri somutlaştırılacak ve bağlama adımı düzgün çalışacaktır.


7
Pratikte, çoğu insan bunun için ayrı bir cpp dosyası kullanır - stackinstantiations.cpp gibi bir şey.
Nemanja Trifunovic

@NemanjaTrifunovic stackinstantiations.cpp'nin nasıl görüneceğine dair bir örnek verebilir misiniz?
qwerty9967

3
Aslında başka çözümler de var: codeproject.com/Articles/48575/…
sleepsort

@ Benoît Bir hata hatası aldım: ';' öncesinde nitelenmemiş kimlik bekleniyordu belirteç şablon yığını <int>; Neden biliyor musun? Teşekkürler!
camino

3
Aslında, doğru sözdizimi template class stack<int>;.
Paul Baltescu

8

Bu şekilde yapabilirsin

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Bu Daniweb'de tartışıldı

Ayrıca SSS'de ancak C ++ dışa aktarma anahtar kelimesini kullanıyor.


5
includeBir cppdosyayı kullanmak genellikle kötü bir fikirdir. bunun için geçerli bir nedeniniz olsa bile, dosyaya - gerçekten sadece yüceltilmiş bir başlıktır - neler olup bittiğini çok netleştirmek, gerçek dosyaları hedefleme konusundaki kafa karışıklığını gidermek , vb. için hppfarklı bir uzantı (örneğin tpp) verilmelidir .makefile cpp
underscore_d

@underscore_d Bir .cppdosya eklemenin neden korkunç bir fikir olduğunu açıklayabilir misiniz ?
Abbas

1
@Abbas çünkü uzantı cpp(veya ccveya cveya her neyse) dosyanın uygulamanın bir parçası olduğunu, ortaya çıkan çeviri biriminin (önişlemci çıktısının) ayrı olarak derlenebileceğini ve dosyanın içeriğinin yalnızca bir kez derlendiğini gösterir. dosyanın keyfi olarak herhangi bir yere dahil edilebilecek arabirimin yeniden kullanılabilir bir parçası olduğunu göstermez. #includeBir ing fiili cpp dosyasını hızlı bir şekilde haklı olarak birden çok tanım hataların bulunduğu ekranı dolduracak ve olacaktır. Orada olarak bu durumda, olduğu için bir neden #includebunun, cppuzatma sadece yanlış bir seçim oldu.
underscore_d

@underscore_d Yani .cppuzantıyı böyle bir kullanım için kullanmak temelde yanlıştır . Ancak başka bir söz kullanmak .tpptamamen uygundur, hangisi aynı amaca hizmet eder, ancak daha kolay / daha hızlı anlamak için farklı bir uzantı kullanır mı?
Abbas

1
@Abbas Evet, cpp/ cc/ etc'den kaçınılmalıdır, ancak başka bir şey kullanmak iyi bir fikirdir hpp- ör tpp. tcc, Vb. - böylece dosya adının geri kalanını yeniden kullanabilir ve tppdosyanın başlık gibi davranmasına rağmen dosyanın olduğunu belirtebilirsiniz. karşılık gelen şablon bildirimlerinin satır dışı uygulamasını tutar hpp. Bu nedenle, bu gönderi iyi bir öncülle başlar - bildirimleri ve tanımları 2 farklı dosyaya ayırarak başlar; bu, grok / grep daha kolay olabilir veya bazen IME döngüsel bağımlılıklar nedeniyle gereklidir - ancak daha sonra 2. dosyanın yanlış bir uzantıya sahip olduğunu öne sürerek kötü bir şekilde sona erer
underscore_d

6

Hayır, mümkün değil. exportTüm niyetler ve amaçlar için gerçekten var olmayan anahtar kelime olmadan olmaz.

Yapabileceğiniz en iyi şey, işlev uygulamalarınızı bir ".tcc" veya ".tpp" dosyasına koymak ve .hpp dosyanızın sonuna .tcc dosyasını dahil etmektir. Ancak bu sadece kozmetiktir; hala başlık dosyalarındaki her şeyi uygulamakla aynıdır. Bu, şablonları kullanmak için ödediğiniz fiyattır.


3
Cevabınız doğru değil. Hangi şablon argümanlarının kullanılacağını bildiğiniz takdirde, bir cpp dosyasındaki bir şablon sınıfından kod üretebilirsiniz. Daha fazla bilgi için cevabıma bakın.
Benoît

2
Doğru, ancak bu, .cpp dosyasını güncellemeye ve şablonu kullanan yeni bir tür her tanıtıldığında yeniden derlemeye ihtiyaç duymanın ciddi bir kısıtlamasıyla birlikte gelir, muhtemelen OP'nin aklında olan bu değildir.
Charles Salvia

3

Sadece #include "stack.cppsonunda olursan stack.hpp. Bu yaklaşımı yalnızca uygulama görece büyükse ve .cpp dosyasını normal koddan farklılaştırmak için başka bir uzantıya yeniden adlandırırsanız öneririm.


4
Bunu yapıyorsanız, stack.cpp dosyanıza #ifndef STACK_CPP (ve arkadaşları) eklemek isteyeceksiniz.
Stephen Newell

Beni bu öneriye yen. Ben de bu yaklaşımı stil nedenleriyle tercih etmiyorum.
luke

2
Evet, böyle bir durumda, 2. dosyaya kesinlikle uzantı cpp(veya ccveya her neyse) verilmemelidir çünkü bu, gerçek rolüyle tam bir tezat oluşturuyor. Bunun yerine, bir başlık ve (B), bir başlık dahil olmak üzere 's (A) belirtir farklı bir uzantı verilmelidir alt başka başlığının. Bunun için kullanıyorum tpp, ki bu da kolaylıkla tem pgeç uygulama p(hat dışı tanımlar) anlamına gelebilir . : Ben daha buraya bu konuda rambled stackoverflow.com/questions/1724036/...
underscore_d

3

Şablonlu kodu bir başlık ve bir cpp olarak ayırmaya çalışmanın iki ana nedeni olduğuna inanıyorum:

Biri sadece zarafet içindir. Hepimiz okunması, yönetilmesi ve daha sonra tekrar kullanılabilen bir kod yazmayı severiz.

Diğeri, derleme sürelerinin kısaltılmasıdır.

Şu anda (her zaman olduğu gibi) simülasyon yazılımını OpenCL ile birlikte kodluyorum ve kodu, donanım kapasitesine bağlı olarak gerektiğinde float (cl_float) veya double (cl_double) kullanarak çalıştırılabilmesi için saklamayı seviyoruz. Şu anda bu, kodun başında bir #define REAL kullanılarak yapılıyor, ancak bu çok zarif değil. İstenilen hassasiyeti değiştirmek, uygulamanın yeniden derlenmesini gerektirir. Gerçek çalışma zamanı türleri olmadığından, şimdilik bununla yaşamak zorundayız. Neyse ki OpenCL çekirdekleri çalışma zamanı derlenmiştir ve basit bir sizeof (GERÇEK) çekirdek kodu çalışma zamanını buna göre değiştirmemize izin verir.

Daha büyük sorun, uygulama modüler olsa bile, yardımcı sınıflar geliştirirken (simülasyon sabitlerini önceden hesaplayanlar gibi) da şablon haline getirilmesi gerektiğidir. Bu sınıfların tümü, sınıf bağımlılığı ağacının en üstünde en az bir kez görünür, çünkü son şablon sınıfı Simulation, bu fabrika sınıflarından birinin bir örneğine sahip olacaktır, yani pratik olarak fabrika sınıfında her küçük değişiklik yaptığımda, yazılımın yeniden oluşturulması gerekiyor. Bu çok can sıkıcı, ancak daha iyi bir çözüm bulamıyorum.


2

Bazen uygulamanın çoğunun cpp dosyasında gizlenmesi mümkündür, eğer ortak işlevsellik foo tüm şablon parametrelerini şablon dışı sınıfa çıkarabilirseniz (muhtemelen tür güvensizdir). Ardından başlık, bu sınıfa yönelik yeniden yönlendirme çağrılarını içerecektir. "Şablon bloat" problemi ile savaşırken benzer bir yaklaşım kullanılır.


+1 - çoğu zaman çok iyi sonuçlanmasa da (en azından istediğim kadar sık ​​değil)
peterchen

2

Yığınınızın hangi türlerle kullanılacağını biliyorsanız, bunları cpp dosyasında açıkça başlatabilir ve ilgili tüm kodu orada tutabilirsiniz.

Bunları DLL'ler arasında (!) Dışa aktarmak da mümkündür, ancak sözdizimini doğru elde etmek oldukça zordur (__declspec (dllexport) ve dışa aktarma anahtar kelimesinin MS'ye özgü kombinasyonları).

Bunu, double / float şablonuna sahip bir matematik / jeom kitaplığında kullandık, ancak oldukça fazla kodumuz vardı. (O sırada Google'da araştırdım, bugün bu koda sahip değilim.)


2

Sorun şu ki, bir şablon gerçek bir sınıf oluşturmaz, sadece derleyiciye nasıl sınıf oluşturacağını söyleyen bir şablondur . Somut bir sınıf oluşturmanız gerekiyor.

Kolay ve doğal yol, yöntemleri başlık dosyasına yerleştirmektir. Ama başka bir yol var.

.Cpp dosyanızda, ihtiyacınız olan her şablon somutlaştırması ve yöntemi için bir referansınız varsa, derleyici bunları projeniz boyunca kullanmak üzere orada üretecektir.

yeni stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

8
Dummey işlevine ihtiyacınız yok: 'şablon yığını <int>' kullanın; Bu, mevcut derleme ünitesine şablonun bir inşasını zorlar. Bir şablon tanımlarsanız ancak paylaşılan bir kütüphanede yalnızca birkaç özel uygulama istiyorsanız çok kullanışlıdır.
Martin York

@Martin: tüm üye işlevleri dahil mi? Bu harika. Bu öneriyi "gizli C ++ özellikleri" iş parçacığına eklemelisiniz.
Mark Ransom

@LokiAstari Birinin daha fazla bilgi edinmek istemesi
Andrew Larsson

1

Hpp dosyasında her şeye sahip olmanız gerekir. Sorun şu ki, derleyici bazı DİĞER cpp dosyaları tarafından ihtiyaç duyulduğunu görene kadar sınıfların gerçekten yaratılmamasıdır - bu nedenle o anda şablonlu sınıfı derlemek için tüm koda sahip olması gerekir.

Yapma eğiliminde olduğum bir şey, şablonlarımı genel bir şablon olmayan bölüme (cpp / hpp arasında bölünebilir) ve şablonlu olmayan sınıfı miras alan türe özgü şablon parçasına bölmeye çalışmaktır.


1

Bunu yapmak isteyebileceğiniz yer, bir kitaplık ve başlık kombinasyonu oluşturduğunuzda ve uygulamayı kullanıcıya gizlediğiniz zamandır. Bu nedenle, önerilen yaklaşım, açık somutlaştırma kullanmaktır, çünkü yazılımınızın ne sunması beklendiğini bilirsiniz ve uygulamaları gizleyebilirsiniz.

Bazı yararlı bilgiler burada: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Aynı örneğiniz için: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Çıktı:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

Bununla birlikte, bu yaklaşımdan tamamen hoşlanmıyorum, çünkü bu, uygulamanın şablon sınıfa yanlış veri türlerini geçirerek kendini ayağa kaldırmasına izin veriyor. Örneğin, main işlevinde, s.Push (1.2) gibi örtük olarak int türüne dönüştürülebilen diğer türleri iletebilirsiniz; ve bu bence çok kötü.


Açık şablon
örneğine

0

Şablonlar gerektiğinde derlendiğinden, bu çok dosyalı projeler için bir kısıtlama zorlar: bir şablon sınıfının veya işlevinin uygulaması (tanımı), bildirimiyle aynı dosyada olmalıdır. Bu, arayüzü ayrı bir başlık dosyasında ayıramayacağımız ve şablonları kullanan herhangi bir dosyaya hem arayüzü hem de uygulamayı dahil etmemiz gerektiği anlamına gelir.


0

Başka bir olasılık da şöyle bir şey yapmaktır:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

Tarz olarak bu öneriden hoşlanmıyorum ama size uyabilir.


1
Dahil edilen yüceltilmiş 2. başlığın cpp, gerçek kaynak dosyalar ile karışıklığı önlemek dışında en azından bir uzantısı olmalıdır . Yaygın öneriler arasında tppve tcc.
underscore_d


0

1) .h ve .cpp dosyalarını ayırmanın ana nedeninin, sınıf uygulamasını, sınıfın bir .h'sini içeren kullanıcının koduna bağlanabilen ayrı olarak derlenmiş bir Obj kodu olarak gizlemek olduğunu unutmayın.

2) Şablon olmayan sınıfların tüm değişkenleri somut olarak ve özellikle .h ve .cpp dosyalarında tanımlanmıştır. Dolayısıyla, derleyici, derleme / çevirmeden önce sınıfta kullanılan tüm veri türleri hakkında ihtiyaç bilgilerine sahip olacaktır - nesne / makine kodunu oluşturma Şablon sınıfları, sınıfın kullanıcısı gerekli verileri ileten bir nesneyi başlatmadan önce belirli veri türü hakkında hiçbir bilgiye sahip değildir. türü:

        TClass<int> myObj;

3) Yalnızca bu somutlaştırmadan sonra, derleyici, geçirilen veri türleriyle eşleşmek için şablon sınıfının belirli sürümünü üretir.

4) Bu nedenle, .cpp, kullanıcıya özel veri türü bilinmeden ayrı olarak DERLENEMEZ. Bu nedenle, kullanıcı gerekli veri türünü belirtene kadar ".h" içinde kaynak kod olarak kalmalıdır, ardından belirli bir veri türüne göre üretilebilir ve ardından derlenebilir


-3

Visual Studio 2010 ile çalışıyorum, dosyalarınızı .h ve .cpp'ye bölmek istiyorsanız, cpp başlığınızı .h dosyasının sonuna ekleyin

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.