C ++ Metodo preferito di gestione dell'implementazione per modelli di grandi dimensioni


10

In genere quando si dichiara una classe C ++, è consigliabile inserire solo la dichiarazione nel file di intestazione e l'implementazione in un file di origine. Tuttavia, sembra che questo modello di progettazione non funzioni per le classi di modelli.

Quando si guarda online sembrano esserci 2 opinioni sul modo migliore per gestire le classi di template:

1. Dichiarazione e attuazione complete nell'intestazione.

Questo è abbastanza semplice ma porta a ciò che, secondo me, è difficile da mantenere e modificare i file di codice quando il modello diventa grande.

2. Scrivi l'implementazione in un modello includi il file (.tpp) incluso alla fine.

Questa mi sembra una soluzione migliore, ma non sembra essere ampiamente applicata. C'è una ragione per cui questo approccio è inferiore?

So che molte volte lo stile del codice è dettato dalle preferenze personali o dallo stile legacy. Sto iniziando un nuovo progetto (porting un vecchio progetto C in C ++) e sono relativamente nuovo nella progettazione di OO e vorrei seguire le migliori pratiche fin dall'inizio.


1
Vedi questo articolo di 9 anni su codeproject.com. Il metodo 3 è quello che hai descritto. Non sembra essere così speciale come credi.
Doc Brown,

.. o qui, stesso approccio, articolo del 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown

2
Strettamente correlato: stackoverflow.com/q/1208028/179910 . Gnu usa in genere un'estensione ".tcc" anziché ".tpp", ma per il resto è praticamente identica.
Jerry Coffin,

Ho sempre usato "ipp" come estensione, ma ho fatto la stessa cosa molto nel codice che ho scritto.
Sebastian Redl,

Risposte:


6

Quando si scrive una classe C ++ basata su modelli, in genere sono disponibili tre opzioni:

(1) Inserire la dichiarazione e la definizione nell'intestazione.

// foo.h
#pragma once

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

o

// foo.h
#pragma once

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

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

Pro:

  • Utilizzo molto conveniente (basta includere l'intestazione).

con:

  • L'implementazione dell'interfaccia e del metodo è mista. Questo è "solo" un problema di leggibilità. Alcuni lo trovano irraggiungibile, perché è diverso dal solito approccio .h / .cpp. Tuttavia, tenere presente che questo non è un problema in altre lingue, ad esempio C # e Java.
  • Elevato impatto sulla ricostruzione: se si dichiara una nuova classe Foocome membro, è necessario includerla foo.h. Ciò significa che la modifica dell'implementazione di si Foo::fpropaga attraverso i file di intestazione e di origine.

Diamo un'occhiata più da vicino all'impatto della ricostruzione: per le classi C ++ non basate su modelli, inserisci le dichiarazioni in .h e le definizioni dei metodi in .cpp. In questo modo, quando viene modificata l'implementazione di un metodo, è necessario ricompilare solo un .cpp. Questo è diverso per le classi di template se .h contiene tutto il tuo codice. Dai un'occhiata al seguente esempio:

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

Qui, l'unico uso di Foo::fè dentro bar.cpp. Tuttavia, se si cambia l'attuazione di Foo::f, sia bar.cppe qux.cppdevono essere ricompilati. L'implementazione di Foo::fvite in entrambi i file, anche se nessuna parte di Quxutilizza direttamente nulla di Foo::f. Per progetti di grandi dimensioni, questo può presto diventare un problema.

(2) Metti la dichiarazione in .h e la definizione in .tpp e includila in .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()
{
    ...
}

Pro:

  • Utilizzo molto conveniente (basta includere l'intestazione).
  • Le definizioni di interfaccia e metodo sono separate.

con:

  • Elevato impatto sulla ricostruzione (uguale a (1) ).

Questa soluzione separa la dichiarazione e la definizione del metodo in due file separati, proprio come .h / .cpp. Tuttavia, questo approccio presenta lo stesso problema di ricostruzione di (1) , poiché l'intestazione include direttamente le definizioni del metodo.

(3) Inserisci la dichiarazione in .h e la definizione in .tpp, ma non includere .tpp in .h.

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

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

Pro:

  • Riduce l'impatto della ricostruzione proprio come la separazione .h / .cpp.
  • Le definizioni di interfaccia e metodo sono separate.

con:

  • Utilizzo non conforme: quando si aggiunge un Foomembro a una classe Bar, è necessario includerlo foo.hnell'intestazione. Se chiami Foo::fun .cpp, devi anche includerlo foo.tpp.

Questo approccio riduce l'impatto della ricostruzione, poiché è Foo::fnecessario ricompilare solo i file .cpp che utilizzano realmente . Tuttavia, questo ha un prezzo: tutti questi file devono essere inclusi foo.tpp. Prendi l'esempio dall'alto e usa il nuovo approccio:

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

Come puoi vedere, l'unica differenza è l'inclusione aggiuntiva di foo.tppin bar.cpp. Questo è scomodo e aggiungere una seconda inclusione per una classe a seconda che tu chiami metodi su di essa sembra molto brutto. Tuttavia, si riduce l'impatto della ricostruzione: bar.cppdeve essere ricompilato solo se si modifica l'implementazione di Foo::f. Il file qux.cppnon necessita di ricompilazione.

Sommario:

Se si implementa una libreria, di solito non è necessario preoccuparsi della ricostruzione dell'impatto. Gli utenti della tua libreria prendono una versione e la usano e l'implementazione della libreria non cambia nel lavoro quotidiano dell'utente. In questi casi, la libreria può usare l'approccio (1) o (2) ed è solo una questione di gusti quale scegliere.

Tuttavia, se stai lavorando su un'applicazione o se stai lavorando su una libreria interna della tua azienda, il codice cambia frequentemente. Quindi devi preoccuparti di ricostruire l'impatto. La scelta dell'approccio (3) può essere una buona opzione se fai in modo che i tuoi sviluppatori accettino l'inclusione aggiuntiva.


2

Simile .tppall'idea (che non ho mai visto usato), mettiamo la maggior parte delle funzionalità incorporate in un -inl.hppfile che è incluso alla fine del solito .hppfile.

Come altri indicano, questo mantiene leggibile l'interfaccia spostando il disordine delle implementazioni inline (come i modelli) in un altro file. Consentiamo alcune linee di interfaccia, ma proviamo a limitarle a funzioni piccole, in genere a linea singola.


1

Una moneta pro della seconda variante è che le intestazioni sembrano più ordinate.

Il problema potrebbe essere che potresti avere il controllo degli errori IDE in linea e le associazioni del debugger sbagliate.


2nd richiede anche molta ridondanza della dichiarazione dei parametri del template, che può diventare molto dettagliata soprattutto quando si usa sfinae. E contrariamente all'OP, trovo il 2 ° più difficile da leggere più codice c'è, in particolare a causa del ridondante boilerplate.
Sopel,

0

Preferisco di gran lunga l'approccio di mettere l'implementazione in un file separato e di avere solo la documentazione e le dichiarazioni nel file di intestazione.

Forse il motivo per cui non hai visto questo approccio usato molto nella pratica, è che non hai guardato nei posti giusti ;-)

O forse è perché ci vuole un piccolo sforzo in più nello sviluppo del software. Ma per una biblioteca di classe, vale la pena fare questo sforzo, IMHO, e si ripaga da sola in una biblioteca molto più facile da usare / leggere.

Prendi ad esempio questa libreria: https://github.com/SophistSolutions/Stroika/

L'intera libreria è scritta con questo approccio e se guardi attraverso il codice, vedrai quanto funziona bene.

I file di intestazione sono lunghi quanto i file di implementazione, ma non sono altro che dichiarazioni e documentazione.

Confronta la leggibilità di Stroika con quella della tua implementazione preferita di std c ++ (gcc o libc ++ o msvc). Tutti usano l'approccio di implementazione in-header incorporato e, sebbene siano estremamente ben scritti, IMHO, non come implementazioni leggibili.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.