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

3

Tipicamente quando si dichiara una classe C ++, è consigliabile mettere solo la dichiarazione nel file di intestazione e mettere l'implementazione in un file sorgente. Tuttavia, sembra che questo modello di progettazione non funzioni per le classi template.

Quando guardi online, ci sono 2 opinioni sul modo migliore di gestire le classi template:

1. Intera dichiarazione e implementazione nell'intestazione.

Questo è abbastanza semplice, ma porta a quelli che, secondo me, sono difficili da mantenere e modificare i file di codice quando il template diventa grande.

2. Scrivi l'implementazione in un modello include 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 da preferenze personali o stile legacy. Sto iniziando un nuovo progetto (porting un vecchio progetto C in C ++) e sono relativamente nuovo alla progettazione OO e vorrei seguire le migliori pratiche fin dall'inizio.

    
posta fhorrobin 10.07.2018 - 19:43
fonte

4 risposte

1

Quando scrivi una classe C ++ basata su modelli, di solito hai tre opzioni:

(1) Inserisci dichiarazione e 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'interfaccia e l'implementazione del metodo sono misti. Questo è "solo" un problema di leggibilità. Alcuni lo trovano irraggiungibile, perché è diverso dal solito approccio .h / .cpp. Tuttavia, tieni presente che questo non è un problema in altre lingue, ad esempio C # e Java.
  • Alto impatto di ricostruzione: se dichiari una nuova classe con Foo come membro, devi includere foo.h . Ciò significa che la modifica dell'implementazione di Foo::say viene propagata attraverso i file di intestazione e di origine.

Diamo un'occhiata più da vicino all'impatto della ricostruzione: Per le classi C ++ non-templated, si mettono 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 uno .cpp. Questo è diverso per le classi template se il file .h contiene tutto il 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 utilizzo di Foo:f è all'interno di bar.cpp . Tuttavia, se cambi l'implementazione di Foo::f , sia la bar.cpp che la qux.cpp devono essere ricompilate. L'implementazione di Foo::f è presente in entrambi i file, anche se nessuna parte di Qux utilizza direttamente qualcosa di Foo::f . Per i 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:

  • Alto impatto di 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) , perché l'intestazione include direttamente le definizioni del metodo.

(3) Inserisci dichiarazione in .h e 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 scomodo: quando aggiungi un membro Foo a una classe Bar , devi includere foo.h nell'intestazione. Se chiami Foo::f in un file .cpp, anche devi includere foo.tpp lì.

Questo approccio riduce l'impatto della ricostruzione, poiché è necessario ricompilare solo i file .cpp che utilizzano veramente Foo::f . Tuttavia, questo ha un prezzo: tutti questi file devono includere foo.tpp . Prendi l'esempio dall'alto e utilizza 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.tpp in bar.cpp . Ciò è inopportuno e aggiungere una seconda inclusione per una classe a seconda che si chiamino i metodi su di essa sembra molto brutto. Tuttavia, riduci l'impatto della ricostruzione: solo bar.cpp deve essere ricompilato se cambi l'implementazione di Foo::f . Il file qux.cpp non ha bisogno di ricompilazione.

Riepilogo:

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

Tuttavia, se quando si lavora su un progetto come un'applicazione o se la libreria è un progetto interno della propria azienda, il codice cambia frequentemente. Quindi devi preoccuparti dell'impatto della ricostruzione. La scelta dell'approccio (3) può essere una buona opzione se chiedi agli sviluppatori di accettare l'inclusione aggiuntiva.

    
risposta data 17.07.2018 - 10:45
fonte
2

Simile all'idea .tpp (che non ho mai visto usato), inseriamo la maggior parte delle funzionalità inline in un file -inl.hpp che è incluso alla fine del solito file .hpp .

Come indicato da altri, ciò mantiene l'interfaccia leggibile spostando la confusione delle implementazioni in linea (come i modelli) in un altro file. Consentiamo alcune interfacce in linea, ma proviamo a limitarle a funzioni di piccole dimensioni, tipicamente a linea singola.

    
risposta data 11.07.2018 - 17:14
fonte
1

Una moneta pro della 2a variante è che le tue intestazioni appaiono più ordinate.

Potresti avere il controllo degli errori IDE in linea e il binding del debugger è fallito.

    
risposta data 10.07.2018 - 20:09
fonte
0

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

Forse la ragione per cui non hai visto questo approccio è stata usata in pratica molto, non hai guardato nei posti giusti; -)

Oppure - forse perché richiede un piccolo sforzo in più nello sviluppo del software. Ma per una libreria di classi, questo sforzo vale BENE, IMHO, e si ripaga da solo in una libreria molto più facile da usare / leggere.

Prendi questa libreria per esempio:       link

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

I file di intestazione sono lunghi quanto i file di implementazione, ma sono riempiti con nient'altro che dichiarazioni e documentazione.

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

    
risposta data 17.07.2018 - 02:47
fonte

Leggi altre domande sui tag