C ++ Metode yang disukai berurusan dengan implementasi untuk template besar

10

Biasanya ketika mendeklarasikan kelas C ++, itu adalah praktik terbaik untuk hanya menempatkan deklarasi dalam file header dan meletakkan implementasinya dalam file sumber. Namun, tampaknya model desain ini tidak berfungsi untuk kelas templat.

Saat mencari online, tampaknya ada 2 pendapat tentang cara terbaik mengelola kelas templat:

1. Seluruh deklarasi dan implementasi di header.

Ini cukup mudah tetapi mengarah pada apa yang, menurut pendapat saya, sulit untuk mempertahankan dan mengedit file kode ketika template menjadi besar.

2. Tulis implementasinya dalam templat include file (.tpp) yang disertakan di bagian akhir.

Ini sepertinya solusi yang lebih baik bagi saya tetapi tampaknya tidak diterapkan secara luas. Apakah ada alasan mengapa pendekatan ini lebih rendah?

Saya tahu bahwa berkali-kali gaya kode ditentukan oleh preferensi pribadi atau gaya lama. Saya memulai proyek baru (memindahkan proyek C lama ke C ++) dan saya relatif baru dalam desain OO dan ingin mengikuti praktik terbaik dari awal.

fhorrobin
sumber
1
Lihat artikel berumur 9 tahun ini di codeproject.com. Metode 3 adalah apa yang Anda gambarkan. Sepertinya tidak seistimewa yang Anda yakini.
Doc Brown
.. atau di sini, pendekatan yang sama, artikel dari 2014: codeofhonour.blogspot.com/2014/11/…
Doc Brown
2
Terkait erat: stackoverflow.com/q/1208028/179910 . Gnu biasanya menggunakan ekstensi ".tcc" alih-alih ".tpp", tetapi sebaliknya hampir sama.
Jerry Coffin
Saya selalu menggunakan "ipp" sebagai ekstensi, tetapi saya sering melakukan hal yang sama pada kode yang saya tulis.
Sebastian Redl

Jawaban:

6

Saat menulis kelas C ++ templated, Anda biasanya memiliki tiga opsi:

(1) Masukkan deklarasi dan definisi di header.

// foo.h
#pragma once

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

atau

// foo.h
#pragma once

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

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

Pro:

  • Penggunaan yang sangat nyaman (cukup sertakan tajuk).

Menipu:

  • Implementasi antarmuka dan metode dicampur. Ini "hanya" masalah keterbacaan. Beberapa menemukan ini tidak dapat dipertahankan, karena berbeda dari pendekatan .h / .cpp yang biasa. Namun, perlu diketahui bahwa ini tidak ada masalah dalam bahasa lain, misalnya, C # dan Java.
  • Dampak pembangunan kembali tinggi: Jika Anda mendeklarasikan kelas baru dengan Foosebagai anggota, Anda harus menyertakan foo.h. Ini berarti bahwa mengubah implementasi Foo::fpropagasi melalui file header dan sumber.

Mari kita melihat lebih dekat pada dampak rekondisi: Untuk kelas-kelas C ++ non-templated, Anda menempatkan deklarasi dalam .h dan definisi metode dalam .cpp. Dengan cara ini, ketika implementasi suatu metode diubah, hanya satu .cpp yang perlu dikompilasi ulang. Ini berbeda untuk kelas templat jika .h berisi semua kode Anda. Lihatlah contoh berikut:

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

Di sini, satu-satunya penggunaan Foo::fdi dalam bar.cpp. Namun, jika Anda mengubah implementasi Foo::f, keduanya bar.cppdan qux.cppperlu dikompilasi ulang. Implementasi Foo::fkehidupan di kedua file, meskipun tidak ada bagian dari Quxlangsung menggunakan apa pun Foo::f. Untuk proyek-proyek besar, ini bisa segera menjadi masalah.

(2) Masukkan deklarasi dalam .h dan definisi dalam .tpp dan sertakan dalam .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:

  • Penggunaan yang sangat nyaman (cukup sertakan tajuk).
  • Antarmuka dan definisi metode dipisahkan.

Menipu:

  • Dampak pembangunan kembali tinggi (sama dengan (1) ).

Solusi ini memisahkan deklarasi dan definisi metode dalam dua file terpisah, sama seperti .h / .cpp. Namun, pendekatan ini memiliki masalah pembangunan kembali yang sama dengan (1) , karena header langsung memasukkan definisi metode.

(3) Masukkan deklarasi dalam .h dan definisi dalam .tpp, tapi jangan sertakan .tpp dalam .h.

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

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

Pro:

  • Mengurangi dampak rekondisi sama seperti pemisahan .h / .cpp.
  • Antarmuka dan definisi metode dipisahkan.

Menipu:

  • Penggunaan tidak nyaman: Saat menambahkan Fooanggota ke kelas Bar, Anda harus memasukkan foo.hdalam header. Jika Anda memanggil Foo::f.cpp, Anda juga harus memasukkannya foo.tpp.

Pendekatan ini mengurangi dampak pembangunan kembali, karena hanya file .cpp yang benar-benar digunakan Foo::fperlu dikompilasi ulang. Namun, ini ada harganya: Semua file itu perlu disertakan foo.tpp. Ambil contoh dari atas dan gunakan pendekatan baru:

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

Seperti yang Anda lihat, satu-satunya perbedaan adalah tambahan termasuk foo.tppdalam bar.cpp. Ini tidak nyaman dan menambahkan menyertakan kedua untuk kelas tergantung pada apakah Anda memanggil metode itu tampak sangat jelek. Namun, Anda mengurangi dampak pembangunan kembali: Hanya bar.cppperlu dikompilasi ulang jika Anda mengubah implementasi Foo::f. File qux.cpptidak perlu dikompilasi ulang.

Ringkasan:

Jika Anda menerapkan pustaka, Anda biasanya tidak perlu peduli untuk membangun kembali dampak. Pengguna perpustakaan Anda mengambil rilis dan menggunakannya dan implementasi perpustakaan tidak berubah dalam pekerjaan sehari-hari pengguna. Dalam kasus seperti itu, perpustakaan dapat menggunakan pendekatan (1) atau (2) dan itu hanya masalah selera mana yang Anda pilih.

Namun, jika Anda mengerjakan aplikasi, atau jika Anda bekerja di perpustakaan internal perusahaan Anda, kode sering berubah. Jadi, Anda harus peduli tentang dampak pembangunan kembali. Memilih pendekatan (3) bisa menjadi pilihan yang baik jika Anda membuat pengembang Anda menerima tambahan yang disertakan.

pschill
sumber
2

Mirip dengan .tppide (yang belum pernah saya lihat digunakan), kami menempatkan sebagian besar fungsi inline ke dalam -inl.hppfile yang termasuk di akhir .hppfile biasa .

Seperti yang ditunjukkan orang lain, ini membuat antarmuka dapat dibaca dengan memindahkan kekacauan implementasi inline (seperti templat) di file lain. Kami mengizinkan beberapa inline antarmuka tetapi mencoba membatasi mereka untuk fungsi-fungsi garis kecil, biasanya tunggal.

Bill Door
sumber
1

Satu koin pro dari varian ke-2 adalah tajuk Anda terlihat lebih rapi.

Con mungkin adalah Anda mungkin memiliki pengecekan kesalahan IDE inline, dan binding debugger kacau.

πάντα ῥεῖ
sumber
2nd juga membutuhkan banyak redundansi deklarasi parameter template, yang dapat menjadi sangat bertele-tele terutama ketika menggunakan sfinae. Dan bertentangan dengan OP saya menemukan 2 lebih sulit untuk membaca lebih banyak kode yang ada, khususnya karena boilerplate yang berlebihan.
Sopel
0

Saya sangat suka pendekatan menempatkan implementasi dalam file terpisah, dan hanya memiliki dokumentasi dan deklarasi dalam file header.

Mungkin alasan Anda belum melihat pendekatan ini banyak digunakan dalam praktik, adalah Anda belum melihat di tempat yang tepat ;-)

Atau - mungkin karena butuh sedikit usaha ekstra dalam mengembangkan perangkat lunak. Tetapi untuk perpustakaan kelas, upaya itu bernilai WELL sementara, IMHO, dan membayar sendiri dalam perpustakaan yang jauh lebih mudah digunakan / dibaca.

Ambil perpustakaan ini sebagai contoh: https://github.com/SophistSolutions/Stroika/

Seluruh perpustakaan ditulis dengan pendekatan ini dan jika Anda melihat melalui kode, Anda akan melihat seberapa baik kerjanya.

File header kira-kira sepanjang file implementasi, tetapi mereka tidak berisi apa-apa selain deklarasi dan dokumentasi.

Bandingkan keterbacaan Stroika dengan implementasi std c ++ favorit Anda (gcc atau libc ++ atau msvc). Mereka semua menggunakan pendekatan implementasi in-header inline, dan meskipun mereka ditulis dengan sangat baik, IMHO, bukan sebagai implementasi yang dapat dibaca.

Lewis Pringle
sumber