C ++ Preferowana metoda radzenia sobie z implementacją dużych szablonów

10

Zazwyczaj deklarując klasę C ++, najlepszą praktyką jest umieszczanie tylko deklaracji w pliku nagłówkowym i implementacji w pliku źródłowym. Wydaje się jednak, że ten model projektowy nie działa w przypadku klas szablonów.

Podczas wyszukiwania online wydaje się, że są 2 opinie na temat najlepszego sposobu zarządzania klasami szablonów:

1. Cała deklaracja i wdrożenie w nagłówku.

Jest to dość proste, ale prowadzi do tego, co moim zdaniem jest trudne do utrzymania i edycji plików kodu, gdy szablon staje się duży.

2. Napisz implementację w szablonie dołączonym na końcu plikiem (.tpp).

Wydaje mi się to lepszym rozwiązaniem, ale nie wydaje się być szeroko stosowane. Czy istnieje powód, dla którego takie podejście jest gorsze?

Wiem, że wiele razy styl kodu jest podyktowany osobistymi preferencjami lub starszym stylem. Zaczynam nowy projekt (przenoszę stary projekt C do C ++) i jestem stosunkowo nowy w projektowaniu OO i od samego początku chciałbym stosować się do najlepszych praktyk.

forrobrobin
źródło
1
Zobacz ten 9-letni artykuł na codeproject.com. Metoda 3 jest tym, co opisałeś. Nie wydaje się być tak wyjątkowy, jak myślisz.
Doc Brown
.. lub tutaj, to samo podejście, artykuł z 2014 roku: codeofhonour.blogspot.com/2014/11/…
Doc Brown
2
Ściśle powiązane: stackoverflow.com/q/1208028/179910 . GNU zwykle używa rozszerzenia „.tcc” zamiast „.tpp”, ale poza tym jest prawie identyczne.
Jerry Coffin
Zawsze użyłem „ipp” jako rozszerzenia, ale zrobiłem to samo dużo w kodzie, który napisałem.
Sebastian Redl

Odpowiedzi:

6

Pisząc szablonową klasę C ++, zazwyczaj masz trzy opcje:

(1) Umieść deklarację i definicję w nagłówku.

// foo.h
#pragma once

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

lub

// foo.h
#pragma once

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

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

Zawodowiec:

  • Bardzo wygodne użycie (wystarczy dołączyć nagłówek).

Kon:

  • Implementacja interfejsu i metody jest mieszana. Jest to „tylko” problem z czytelnością. Niektórzy uważają to za niemożliwe do utrzymania, ponieważ różni się ono od zwykłego podejścia .h / .cpp. Należy jednak pamiętać, że nie stanowi to problemu w innych językach, na przykład C # i Java.
  • Duży wpływ na odbudowę: jeśli deklarujesz nową klasę Foojako członek, musisz ją uwzględnić foo.h. Oznacza to, że zmiana implementacji Foo::fpropagacji odbywa się zarówno przez pliki nagłówkowe, jak i źródłowe.

Przyjrzyjmy się bliżej wpływowi przebudowy: w przypadku nieszablonowanych klas C ++ deklaracje umieszczasz w .h, a definicje metod w .cpp. W ten sposób, gdy implementacja metody zostanie zmieniona, tylko jeden plik .cpp musi zostać ponownie skompilowany. Jest inaczej w przypadku klas szablonów, jeśli .h zawiera cały kod. Spójrz na następujący przykład:

// 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();
}

Tutaj jedynym zastosowaniem Foo::fjest wnętrze bar.cpp. Jeśli jednak zmienić realizacji Foo::f, zarówno bar.cppi qux.cpppotrzeby rekompilacji. Implementacja Foo::fżycia w obu plikach, nawet jeśli żadna część Quxbezpośrednio z nich nie korzysta Foo::f. W przypadku dużych projektów może to wkrótce stać się problemem.

(2) Umieść deklarację w .h, a definicję w .tpp i dołącz do .h.

// 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()
{
    ...
}

Zawodowiec:

  • Bardzo wygodne użycie (wystarczy dołączyć nagłówek).
  • Definicje interfejsu i metod są rozdzielone.

Kon:

  • Duży wpływ na odbudowę (taki sam jak (1) ).

To rozwiązanie dzieli deklarację i definicję metody na dwa osobne pliki, podobnie jak .h / .cpp. Jednak w tym podejściu występuje ten sam problem z odbudową, co (1) , ponieważ nagłówek zawiera bezpośrednio definicje metod.

(3) Umieść deklarację w .h, a definicję w .tpp, ale nie dołączaj .tpp do .h.

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

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

Zawodowiec:

  • Zmniejsza wpływ odbudowy, podobnie jak separacja .h / .cpp.
  • Definicje interfejsu i metod są rozdzielone.

Kon:

  • Niewygodne użycie: dodając Fooczłonka do klasy Bar, musisz dołączyć go foo.hdo nagłówka. Jeśli wywołujesz Foo::fplik .cpp, musisz tam również dołączyć foo.tpp.

Takie podejście zmniejsza wpływ przebudowy, ponieważ tylko pliki .cpp, które naprawdę korzystają, Foo::fmuszą zostać ponownie skompilowane. Ma to jednak swoją cenę: wszystkie te pliki muszą zostać uwzględnione foo.tpp. Weź przykład z góry i zastosuj nowe podejście:

// 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();
}

Jak widać, jedyną różnicą jest dodatkowe włączenie foo.tppw bar.cpp. Jest to niewygodne, a dodanie drugiej klasy dla klasy w zależności od tego, czy wywołujesz metody, wydaje się bardzo brzydkie. Zmniejszasz jednak wpływ przebudowy: musisz tylko bar.cppponownie skompilować, jeśli zmienisz implementację Foo::f. Plik qux.cppnie wymaga ponownej kompilacji.

Podsumowanie:

Jeśli implementujesz bibliotekę, zwykle nie musisz przejmować się skutkami odbudowy. Użytkownicy Twojej biblioteki pobierają wersję i korzystają z niej, a implementacja biblioteki nie zmienia się w codziennej pracy użytkownika. W takich przypadkach biblioteka może zastosować podejście (1) lub (2) i to tylko kwestia gustu, który wybierzesz.

Jeśli jednak pracujesz nad aplikacją lub pracujesz nad biblioteką wewnętrzną swojej firmy, kod często się zmienia. Musisz więc dbać o wpływ odbudowy. Wybór podejścia (3) może być dobrą opcją, jeśli zachęcisz programistów do zaakceptowania dodatkowego uwzględnienia.

pschill
źródło
2

Podobnie do .tpppomysłu (którego nigdy nie widziałem), umieszczamy większość wbudowanych funkcji w -inl.hpppliku znajdującym się na końcu zwykłego .hpppliku.

Jak wskazują inne, dzięki temu interfejs jest czytelny, przenosząc bałagan implementacji wbudowanych (takich jak szablony) w innym pliku. Zezwalamy na wstawianie niektórych interfejsów, ale staramy się ograniczać je do małych, zwykle jednowierszowych funkcji.

Bill Door
źródło
1

Jedna profesjonalna moneta drugiego wariantu polega na tym, że nagłówki wyglądają na bardziej uporządkowane.

Wadą może być to, że masz wbudowane sprawdzanie błędów IDE, a powiązania debuggera są zepsute.

πάντα ῥεῖ
źródło
2. wymaga również dużej redundancji deklaracji parametrów szablonu, która może stać się bardzo szczegółowa, szczególnie przy użyciu sfinae. I w przeciwieństwie do OP uważam, że 2. trudniej jest odczytać więcej kodu, szczególnie z powodu zbędnej płyty kotłowej.
Sopel,
0

Zdecydowanie wolę podejście polegające na umieszczeniu implementacji w osobnym pliku i posiadaniu tylko dokumentacji i deklaracji w pliku nagłówkowym.

Być może powodem, dla którego nie widziałeś tego podejścia w praktyce, jest to, że nie szukałeś właściwych miejsc ;-)

Lub - być może dlatego, że zajmuje to trochę dodatkowego wysiłku w celu opracowania oprogramowania. Ale w przypadku biblioteki klasowej ten wysiłek jest WOLNY, IMHO, i zwraca się w znacznie łatwiejszej w użyciu / czytaniu bibliotece.

Weźmy tę bibliotekę na przykład: https://github.com/SophistSolutions/Stroika/

Cała biblioteka jest napisana z takim podejściem i jeśli przejrzysz kod, zobaczysz, jak dobrze działa.

Pliki nagłówkowe mają długość plików implementacyjnych, ale są wypełnione jedynie deklaracjami i dokumentacją.

Porównaj czytelność Stroiki z twoją ulubioną implementacją std c ++ (gcc lub libc ++ lub msvc). Wszystkie wykorzystują wbudowane podejście do implementacji w nagłówku i chociaż są wyjątkowo dobrze napisane, IMHO, nie są implementacjami czytelnymi.

Lewis Pringle
źródło