Apakah praktik yang baik untuk mengandalkan header dimasukkan secara transitif?

37

Saya sedang membersihkan yang termasuk dalam proyek C ++ yang sedang saya kerjakan, dan saya terus bertanya-tanya apakah saya harus secara eksplisit menyertakan semua header yang digunakan secara langsung dalam file tertentu, atau apakah saya hanya harus memasukkan minimum yang kosong.

Berikut ini contohnya Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Mari kita asumsikan bahwa deklarasi maju untuk RenderObjectbukan pilihan.)

Sekarang, saya tahu itu RenderObject.hpptermasuk Texture.hpp- Saya tahu itu karena masing-masing RenderObjectmemiliki Textureanggota. Namun, saya secara eksplisit termasuk Texture.hppdalam Entity.hpp, karena saya tidak yakin apakah itu ide yang baik untuk mengandalkan itu termasuk dalam RenderObject.hpp.

Jadi: Apakah ini praktik yang baik atau tidak?

futlib
sumber
19
Di mana termasuk penjaga dalam contoh Anda? Anda hanya melupakannya secara tidak sengaja, saya harap?
Doc Brown
3
Satu masalah yang terjadi ketika Anda tidak memasukkan semua file yang digunakan adalah bahwa kadang-kadang urutan Anda memasukkan file kemudian menjadi penting. Itu benar-benar menjengkelkan hanya dalam satu kasus di mana itu terjadi tetapi kadang-kadang bola salju dan Anda akhirnya benar-benar berharap orang yang menulis kode seperti itu akan berbaris di depan regu tembak.
Dunk
Inilah mengapa ada #ifndef _RENDER_H #define _RENDER_H ... #endif.
sampathsris
@Dunk Saya pikir Anda salah paham masalahnya. Dengan salah satu sarannya itu seharusnya tidak terjadi.
Mooing Duck
1
@DocBrown, #pragma oncemenyelesaikannya, bukan?
Pacerier

Jawaban:

65

Anda harus selalu menyertakan semua tajuk yang mendefinisikan objek apa pun yang digunakan dalam file .cpp dalam file tersebut terlepas dari apa yang Anda ketahui tentang apa yang ada di file-file itu. Anda harus menyertakan penjaga di semua file header untuk memastikan bahwa memasukkan header beberapa kali tidak masalah.

Alasan:

  • Ini menjelaskan kepada pengembang yang membaca sumber persis apa yang diperlukan oleh file sumber tersebut. Di sini, seseorang yang melihat beberapa baris pertama dalam file dapat melihat bahwa Anda berurusan dengan Textureobjek dalam file ini.
  • Ini menghindari masalah di mana header refactored menyebabkan masalah kompilasi ketika mereka tidak lagi membutuhkan header tertentu. Sebagai contoh, misalkan Anda menyadari bahwa RenderObject.hppsebenarnya tidak membutuhkannya Texture.hppsendiri.

Yang wajar adalah bahwa Anda tidak boleh memasukkan header di header lain kecuali jika diperlukan secara eksplisit dalam file itu.

Gort the Robot
sumber
10
Setuju dengan akibat wajar - dengan syarat harus SELALU menyertakan header lainnya jika TIDAK PERLU!
Andrew
1
Saya tidak menyukai praktik menyertakan langsung header untuk semua kelas individu. Saya mendukung tajuk kumulatif. Artinya, saya pikir file tingkat tinggi harus referensi "modul" dari beberapa jenis yang digunakannya, tetapi tidak perlu secara langsung memasukkan semua bagian individu.
edA-qa mort-ora-y
8
Itu mengarah ke header monolitik besar yang mencakup setiap file, bahkan ketika mereka hanya membutuhkan sedikit dari apa yang ada di dalamnya, yang mengarah ke waktu kompilasi yang lama dan membuat refactoring lebih sulit.
Gort the Robot
6
Google telah membangun alat untuk membantu menegakkan saran ini dengan tepat, yang disebut include-what-you-use .
Matius G.
3
Masalah utama waktu kompilasi dengan header monolitik besar bukanlah waktu untuk mengkompilasi kode header itu sendiri tetapi harus mengkompilasi setiap file cpp di aplikasi Anda setiap kali header berubah. Header yang dikompilasi sebelumnya tidak membantu.
Gort the Robot
23

Aturan umum adalah: termasuk apa yang Anda gunakan. Jika Anda menggunakan objek secara langsung, maka sertakan file headernya secara langsung. Jika Anda menggunakan objek A yang menggunakan B tetapi tidak menggunakan B sendiri, hanya sertakan Ah

Juga saat kita sedang membahas topik tersebut, Anda hanya perlu memasukkan file header lainnya dalam file header Anda jika Anda benar-benar membutuhkannya di header. Jika Anda hanya membutuhkannya di .cpp, maka hanya sertakan di sana: ini adalah perbedaan antara ketergantungan publik dan pribadi, dan akan mencegah pengguna kelas Anda dari menyeret header yang sebenarnya tidak mereka butuhkan.

Nir Friedman
sumber
10

Saya terus bertanya-tanya apakah saya harus secara eksplisit menyertakan semua header yang digunakan secara langsung dalam file tertentu

Iya nih.

Anda tidak pernah tahu kapan tajuk lainnya mungkin berubah. Masuk akal di dunia untuk menyertakan, di setiap unit terjemahan, tajuk yang Anda tahu membutuhkan unit terjemahan.

Kami memiliki pelindung tajuk untuk memastikan bahwa penyertaan ganda tidak berbahaya.

Lightness Races with Monica
sumber
3

Pendapat berbeda mengenai hal ini, tetapi saya berpendapat bahwa setiap file (apakah file sumber c / cpp, atau file header h / hpp) harus dapat dikompilasi atau dianalisis sendiri.

Dengan demikian, semua file harus menyertakan # semua dan semua file header yang mereka butuhkan - Anda tidak boleh berasumsi bahwa satu file header sudah termasuk sebelumnya.

Ini sangat menyebalkan jika Anda perlu menambahkan file header dan menemukan bahwa itu menggunakan item yang didefinisikan di tempat lain, tanpa secara langsung memasukkannya ... jadi Anda harus mencari (dan mungkin berakhir dengan yang salah!)

Di sisi lain, itu tidak (sebagai aturan umum) tidak masalah jika Anda # memasukkan file yang tidak Anda butuhkan ...


Sebagai titik gaya pribadi, saya mengatur file #include dalam urutan abjad, dibagi menjadi sistem dan aplikasi - ini membantu memperkuat pesan "mandiri dan sepenuhnya koheren".

Andrew
sumber
Catatan tentang urutan menyertakan: kadang-kadang urutan penting, misalnya saat menyertakan header X11. Ini bisa jadi karena desain (yang mungkin dalam kasus itu dianggap desain buruk), kadang-kadang karena masalah ketidakcocokan yang disayangkan.
hyde
Catatan tentang menyertakan tajuk yang tidak perlu, penting untuk waktu kompilasi, pertama langsung (terutama jika C-template yang berat ++), tetapi terutama ketika menyertakan tajuk proyek yang sama atau ketergantungan di mana file yang disertakan juga berubah, dan akan memicu kompilasi ulang dari semuanya termasuk (jika Anda memiliki dependensi kerja, jika tidak, maka Anda harus melakukan build bersih setiap saat ...).
hyde
2

Itu tergantung pada apakah inklusi transitif itu karena kebutuhan (misalnya kelas dasar) atau karena detail implementasi (anggota pribadi).

Untuk memperjelas, inklusi transitif diperlukan saat menghapusnya hanya dapat dilakukan setelah terlebih dahulu mengubah antarmuka yang dinyatakan dalam header perantara. Karena itu sudah merupakan perubahan yang melanggar, semua file .cpp yang menggunakannya harus tetap diperiksa.

Contoh: Ah dimasukkan oleh Bh yang digunakan oleh C.cpp. Jika Bh menggunakan Ah untuk beberapa detail implementasi, maka C.cpp tidak boleh berasumsi bahwa Bh akan terus melakukannya. Tetapi jika Bh menggunakan Ah untuk kelas dasar, maka C.cpp dapat mengasumsikan bahwa Bh akan terus menyertakan header yang relevan untuk kelas dasarnya.

Anda lihat di sini keuntungan sebenarnya dari TIDAK menduplikasi inklusi tajuk. Katakanlah bahwa kelas dasar yang digunakan oleh Bh benar-benar tidak termasuk dalam Ah dan di refactored ke dalam Bh itu sendiri. Bh sekarang menjadi tajuk mandiri. Jika C.cpp secara berlebihan menyertakan Ah, sekarang termasuk header yang tidak perlu.

MSalters
sumber
2

Mungkin ada kasus lain: Anda memiliki Ah, Bh dan C.cpp Anda, Bh termasuk Ah

jadi di C.cpp, Anda bisa menulis

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Jadi, jika Anda tidak menulis #termasuk "Ah" di sini, apa yang bisa terjadi? dalam C.cpp Anda, baik A dan B (misalnya kelas) digunakan. Kemudian Anda mengubah kode C.cpp Anda, menghapus hal-hal yang terkait B, tetapi membiarkan Bh disertakan di sana.

Jika Anda memasukkan Ah dan Bh dan sekarang pada titik ini, alat yang mendeteksi tidak perlu menyertakan dapat membantu Anda untuk menunjukkan bahwa Bh sertakan tidak lagi diperlukan. Jika Anda hanya menyertakan Bh seperti di atas, maka sulit bagi alat / manusia untuk mendeteksi menyertakan yang tidak perlu setelah perubahan kode Anda.

raja bug
sumber
1

Saya mengambil pendekatan yang sedikit berbeda dari jawaban yang diajukan.

Di header, selalu sertakan hanya minimum, apa yang dibutuhkan untuk membuat kompilasi lulus. Gunakan deklarasi maju sedapat mungkin.

Dalam file sumber, tidak terlalu penting seberapa banyak Anda memasukkan. Preferensi saya masih termasuk minimum untuk membuatnya lulus.

Untuk proyek kecil, termasuk tajuk di sana-sini tidak akan membuat perbedaan. Tetapi untuk proyek menengah hingga besar, itu bisa menjadi masalah. Bahkan jika perangkat keras terbaru digunakan untuk mengkompilasi, perbedaannya dapat terlihat. Alasannya adalah bahwa kompilator masih harus membuka header yang disertakan dan menguraikannya. Jadi, untuk mengoptimalkan build, terapkan teknik di atas (termasuk minimal, dan gunakan forward declare).

Meskipun agak ketinggalan zaman, Desain Perangkat Lunak C ++ Skala Besar (oleh John Lakos) menjelaskan semua ini secara terperinci.

BЈовић
sumber
1
Tidak setuju dengan strategi ini ... jika Anda menyertakan file header dalam file sumber, Anda harus melacak semua dependensinya. Lebih baik memasukkan secara langsung, daripada mencoba dan mendokumentasikan daftar!
Andrew
@Andrew ada alat dan skrip untuk memeriksa apa, dan berapa kali, termasuk.
BЈовић
1
Saya perhatikan optimasi pada beberapa kompiler terbaru untuk menangani ini. Mereka mengenali pernyataan penjaga yang khas, dan memprosesnya. Kemudian, ketika #termasuk lagi, mereka dapat mengoptimalkan pemuatan file sepenuhnya. Namun, rekomendasi Anda untuk deklarasi maju sangat bijaksana untuk mengurangi jumlah yang disertakan. Setelah Anda mulai menggunakan deklarasi maju, itu menjadi keseimbangan waktu berjalan kompiler (ditingkatkan dengan deklarasi maju) dan ramah pengguna (ditingkatkan dengan tambahan nyaman #tercakup), yang merupakan keseimbangan yang disetel oleh setiap perusahaan secara berbeda.
Cort Ammon
1
@CortAmmon Header yang khas memiliki penjaga, tetapi kompiler masih harus membukanya, dan itu adalah operasi yang lambat
Bвовић
4
@ BЈовић: Sebenarnya, tidak. Yang harus mereka lakukan adalah mengenali bahwa file tersebut memiliki penjaga header "khas" dan menandai mereka sehingga hanya membukanya sekali. Gcc, misalnya, memiliki dokumentasi mengenai kapan dan di mana ia menerapkan optimisasi ini: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon
-4

Praktik yang baik adalah jangan khawatir tentang strategi header Anda selama dikompilasi.

Bagian tajuk dari kode Anda hanyalah satu blok garis yang tak seorang pun harus melihatnya sampai Anda mendapatkan kesalahan kompilasi yang mudah diselesaikan. Saya mengerti keinginan untuk gaya yang 'benar', tetapi tidak ada cara yang dapat benar-benar digambarkan sebagai benar. Memasukkan header untuk setiap kelas lebih cenderung menyebabkan kesalahan kompilasi berbasis pesanan yang mengganggu, tetapi kesalahan kompilasi tersebut juga mencerminkan masalah yang dapat diperbaiki oleh pengkodean yang hati-hati (meskipun mereka tidak sepadan dengan waktu untuk memperbaikinya).

Dan ya, Anda akan memiliki masalah berbasis pesanan setelah mulai masuk ke frienddaratan.

Anda dapat memikirkan masalahnya dalam dua kasus.


Kasus 1: Anda memiliki sejumlah kecil kelas yang saling berinteraksi, katakanlah kurang dari selusin. Anda secara teratur menambah, menghapus dari, dan memodifikasi tajuk ini, dengan cara yang dapat memengaruhi ketergantungan mereka satu sama lain. Ini adalah kasus yang disarankan oleh contoh kode Anda.

Kumpulan tajuk cukup kecil sehingga tidak rumit untuk menyelesaikan masalah apa pun yang muncul. Setiap masalah sulit diperbaiki dengan menulis ulang satu atau dua header. Khawatir tentang strategi tajuk Anda adalah menyelesaikan masalah yang tidak ada.


Kasus 2: Anda memiliki banyak kelas. Beberapa kelas mewakili tulang punggung program Anda, dan menulis ulang tajuknya akan memaksa Anda untuk menulis ulang / mengkompilasi ulang sejumlah besar basis kode Anda. Kelas-kelas lain menggunakan tulang punggung ini untuk menyelesaikan banyak hal. Ini mewakili pengaturan bisnis yang khas. Header tersebar di seluruh direktori dan Anda tidak dapat secara realistis mengingat nama-nama segalanya.

Solusi: Pada titik ini, Anda perlu memikirkan kelas Anda dalam kelompok logis dan mengumpulkan kelompok-kelompok itu menjadi header yang menghentikan Anda dari keharusan untuk #includeberulang-ulang. Ini tidak hanya membuat hidup lebih sederhana, itu juga merupakan langkah yang diperlukan untuk mengambil keuntungan dari header yang telah dikompilasi .

Anda berakhir di #includekelas yang tidak Anda butuhkan tetapi siapa yang peduli ?

Dalam hal ini, kode Anda akan terlihat seperti ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
QuestionC
sumber
13
Saya harus -1 ini karena saya jujur ​​percaya kalimat apa pun yang dalam bentuk "Praktek yang baik tidak perlu khawatir tentang strategi ____ Anda selama mengkompilasi" mengarahkan orang ke penilaian yang buruk. Saya telah menemukan bahwa pendekatan mengarah sangat cepat ke arah tidak terbaca, dan tidak terbaca hampir sama buruknya dengan "tidak bekerja." Saya juga menemukan banyak perpustakaan besar yang tidak setuju dengan hasil dari kedua kasus yang Anda gambarkan. Sebagai contoh, Boost DOES melakukan tajuk "koleksi" yang Anda rekomendasikan dalam kasus 2, tetapi mereka juga berupaya menyediakan tajuk kelas-demi-kelas ketika Anda membutuhkannya.
Cort Ammon
3
Saya pribadi menyaksikan "jangan khawatir jika kompilasi" berubah menjadi "aplikasi kita membutuhkan waktu 30 menit untuk mengkompilasi ketika Anda menambahkan nilai pada enum, bagaimana cara kita memperbaikinya !?"
Gort the Robot
Saya membahas masalah waktu kompilasi dalam jawaban saya. Sebenarnya, jawaban saya adalah satu dari hanya dua (tidak ada yang mendapat skor baik) yang memberikan jawaban. Tapi sungguh, itu singgung pertanyaan OP; ini adalah "Haruskah saya unta-huruf nama variabel saya?" ketik pertanyaan. Saya menyadari jawaban saya tidak populer tetapi tidak selalu ada praktik terbaik untuk semuanya, dan ini adalah salah satu kasusnya.
QuestionC
Setuju dengan # 2. Mengenai ide-ide sebelumnya - Saya berharap untuk otomatisasi yang akan memperbarui blok header lokal - sampai saat itu saya menganjurkan daftar lengkap.
chux
Pendekatan "include everything and the kitchen sink" mungkin menghemat waktu Anda awalnya - file header Anda bahkan mungkin terlihat lebih kecil (karena sebagian besar hal disertakan secara tidak langsung dari .... di suatu tempat). Sampai Anda tiba di titik di mana perubahan apa pun di mana saja menyebabkan 30+ menit penyelesaian proyek Anda. Dan autocomplete IDE-smart Anda memunculkan ratusan saran yang tidak relevan. Dan Anda secara tidak sengaja menggabungkan dua kelas yang terlalu mirip atau fungsi statis. Dan Anda menambahkan struct baru tetapi kemudian build gagal karena Anda memiliki tabrakan namespace dengan kelas yang sama sekali tidak berhubungan di suatu tempat ...
CharonX