Büyük şablonlar için uygulama ile ilgilenilen C ++ tercih edilen yöntem


10

Genellikle bir C ++ sınıfını bildirirken, üstbilgi dosyasına yalnızca bildirimi koymak ve uygulamayı bir kaynak dosyasına koymak en iyi yöntemdir. Ancak, bu tasarım modelinin şablon sınıfları için çalışmadığı görülmektedir.

Çevrimiçi bakıldığında, şablon sınıflarını yönetmenin en iyi yolu hakkında 2 görüş var gibi görünüyor:

1. Başlıktaki tüm beyan ve uygulama.

Bu oldukça basittir, ancak bence şablon büyük olduğunda kod dosyalarının bakımı ve düzenlenmesi zor olan şeylere yol açar.

2. Uygulamayı, sonunda bulunan bir şablon içerme dosyası (.tpp) içine yazın.

Bu benim için daha iyi bir çözüm gibi görünüyor, ancak yaygın olarak uygulanmıyor gibi görünüyor. Bu yaklaşımın daha düşük olmasının bir nedeni var mı?

Kod tarzının birçok kez kişisel tercih veya eski tarz tarafından belirlendiğini biliyorum. Yeni bir projeye başlıyorum (eski bir C projesini C ++ 'a taşıyor) ve OO tasarımında nispeten yeniyim ve en iyi uygulamaları en baştan takip etmek istiyorum.


1
Bkz bu 9 yaşındaki makale codeproject.com üzerinde. Yöntem 3, tanımladığınız yöntemdir. İnandığınız kadar özel görünmüyor.
Doc Brown

.. ya da burada, aynı yaklaşım, 2014 makalesi: codeofhonour.blogspot.com/2014/11/…
Doc Brown

2
Yakından ilgili: stackoverflow.com/q/1208028/179910 . Gnu genellikle ".tpp" yerine ".tcc" uzantısını kullanır, ancak aksi halde hemen hemen aynıdır.
Jerry Coffin

Ben her zaman uzantısı olarak "ipp" kullanılır, ama aynı şeyi yazdım kodda çok yaptım.
Sebastian Redl

Yanıtlar:


6

Şablonlu bir C ++ sınıfı yazarken, genellikle üç seçeneğiniz vardır:

(1) Başlığa açıklama ve tanım koyun.

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f()
    {
        ...
    }
};

veya

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f();
};

template <typename T>
inline void Foo::f()
{
    ...
}

Pro:

  • Çok rahat kullanım (sadece başlığı dahil edin).

con:

  • Arayüz ve yöntem uygulaması karışıktır. Bu sadece okunabilirlik problemidir. Bazıları bunu sürdürülemez bulur, çünkü normal .h / .cpp yaklaşımından farklıdır. Ancak, bunun diğer dillerde, örneğin C # ve Java'da sorun olmadığını unutmayın.
  • Yüksek yeniden oluşturma etkisi: FooÜye olarak yeni bir sınıf bildirirseniz , eklemeniz gerekir foo.h. Bu Foo::f, hem üstbilgi hem de kaynak dosyaları aracılığıyla yayılımların uygulanmasının değiştirildiği anlamına gelir .

Yeniden oluşturma etkisine daha yakından bakalım: Şablon olmayan C ++ sınıfları için, .h ve .cpp yöntem tanımlarını koyarsınız. Bu şekilde, bir yöntemin uygulanması değiştirildiğinde, yalnızca bir .cpp dosyasının yeniden derlenmesi gerekir. Bu, .h tüm kodlarınızı içeriyorsa şablon sınıfları için farklıdır. Aşağıdaki örneğe bir göz atın:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Burada, tek kullanımı Foo::fiçeride bar.cpp. Eğer uygulanmasını değiştirirseniz, ancak Foo::f, her iki bar.cppve qux.cppihtiyaç derlenmesi gerekecektir. Her Foo::fiki dosyada da hayatın uygulanması, hiçbir parçası Quxdoğrudan bir şey kullanmasa da Foo::f. Büyük projeler için, bu yakında bir sorun haline gelebilir.

(2) Beyanı .h'ye ve tanımı .tpp'ye koyun ve .h'ye ekleyin.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};
#include "foo.tpp"    

// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
    ...
}

Pro:

  • Çok rahat kullanım (sadece başlığı dahil edin).
  • Arayüz ve yöntem tanımları ayrılır.

con:

  • Yüksek yeniden inşaat etkisi ( (1) ile aynı ).

Bu çözüm, bildirim ve yöntem tanımını, .h / .cpp gibi iki ayrı dosyada ayırır. Ancak, bu yaklaşım (1) ile aynı yeniden oluşturma sorununa sahiptir , çünkü üstbilgi doğrudan yöntem tanımlarını içerir.

(3) .hpp ve .tpp içindeki tanımları belirtin, ancak .h.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};

// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
    ...
}

Pro:

  • .H / .cpp ayrımı gibi yeniden oluşturma etkisini azaltır.
  • Arayüz ve yöntem tanımları ayrılır.

con:

  • Uygunsuz kullanım: Bir Foosınıfa üye eklerken , başlığa Bareklemeniz gerekir foo.h. Foo::fBir .cpp çağırırsanız , oraya da eklemeniz gerekir foo.tpp.

Bu yaklaşım yeniden oluşturma etkisini azaltır, çünkü yalnızca gerçekten kullanılan .cpp dosyalarının Foo::fyeniden derlenmesi gerekir. Ancak, bunun bir bedeli vardır: Tüm bu dosyaların dahil edilmesi gerekir foo.tpp. Yukarıdaki örneği alın ve yeni yaklaşımı kullanın:

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

Gördüğünüz gibi, tek fark, ek bir include foo.tppiçinde bar.cpp. Bu elverişsizdir ve üzerinde yöntem çağırıp çağırmamanıza bağlı olarak sınıf için ikinci bir ekleme eklemek çok çirkin gözükür. Ancak, yeniden oluşturma etkisini azaltırsınız: Yalnızca bar.cppuygulamasını değiştirirseniz yeniden derlenmesi gerekir Foo::f. Dosyanın qux.cppyeniden derlenmesi gerekmez.

Özet:

Bir kitaplık uygularsanız, genellikle yeniden oluşturma etkisini önemsemeniz gerekmez. Kitaplığınızın kullanıcıları bir sürüm alır ve kullanır ve kitaplık uygulaması kullanıcının günlük işlerinde değişmez. Bu gibi durumlarda, kütüphane (1) veya (2) yaklaşımını kullanabilir ve hangisini seçtiğiniz bir zevk meselesidir.

Ancak, bir uygulama üzerinde çalışıyorsanız veya şirketinizin dahili bir kitaplığı üzerinde çalışıyorsanız, kod sık sık değişir. Bu yüzden yeniden inşa etmeyi önemsemelisiniz. Geliştiricilerinizin ek kapsamı kabul etmesini sağlarsanız, yaklaşım (3) seçimi iyi bir seçenek olabilir.


2

.tpp(Daha önce hiç görmediğim) fikrine benzer şekilde, çoğu satır içi işlevselliği -inl.hppnormal .hppdosyanın sonuna dahil edilen bir dosyaya koyduk .

Diğerlerinin belirttiği gibi, bu, satır içi uygulamaların (şablonlar gibi) karmaşasını başka bir dosyada taşıyarak arayüzü okunabilir tutar. Bazı arayüz satırlarına izin veriyoruz, ancak bunları küçük, tipik olarak tek satır işlevleriyle sınırlamaya çalışıyoruz.


1

2. varyantın profesyonel bir parası, başlıklarınızın daha düzenli görünmesidir.

Con, satır içi IDE hata denetimi ve hata ayıklayıcı bağlamaları vidalanmış olabilir.


2 de, özellikle sfinae kullanırken çok ayrıntılı olabilen çok sayıda şablon parametre bildirimi yedekliliği gerektirir. Ve OP aksine, daha fazla kod okumak için daha zor, özellikle gereksiz kazan plakası nedeniyle bulabilirsiniz.
Sopel

0

Uygulamayı ayrı bir dosyaya koymayı ve başlık dosyasında yalnızca dokümantasyon ve bildirimleri bulundurmayı tercih ederim.

Belki de bu yaklaşımı pratikte çok fazla kullanmamanızın nedeni, doğru yerlere bakmamış olmanızdır ;-)

Ya da - belki de yazılımı geliştirmek için biraz fazla çaba gerektirdiği için. Ancak bir sınıf kütüphanesi için, bu çaba WELL'e değerken, IMHO ve kullanımı / okunması kolay bir kütüphanede kendini amorti eder.

Bu kütüphaneyi ele alalım örneğin: https://github.com/SophistSolutions/Stroika/

Tüm kütüphane bu yaklaşımla yazılmıştır ve koda bakarsanız, ne kadar iyi çalıştığını göreceksiniz.

Başlık dosyaları yaklaşık olarak uygulama dosyaları kadar, ancak bildirimler ve belgelerden başka bir şeyle doldurulmamışlardır.

Stroika'nın okunabilirliğini en sevdiğiniz std c ++ uygulamasının (gcc veya libc ++ veya msvc) ile karşılaştırın. Hepsi satır içi satır içi uygulama yaklaşımını kullanır ve son derece iyi yazılmış olmalarına rağmen IMHO, okunabilir uygulamalar olarak değil.

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.