Constexpr vs makro

92

Di mana saya harus memilih menggunakan makro dan di mana saya harus memilih constexpr ? Bukankah pada dasarnya mereka sama?

#define MAX_HEIGHT 720

vs.

constexpr unsigned int max_height = 720;
Tom Dorone
sumber
4
Constexpr AFAIK menyediakan lebih banyak jenis keamanan
Code-Apprentice
13
Mudah: constexr, selalu.
n. 'kata ganti' m.
Mungkin menjawab beberapa pertanyaan Anda stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Jawaban:

147

Bukankah pada dasarnya mereka sama?

Tidak. Sama sekali tidak. Bahkan tidak dekat.

Terlepas dari kenyataan makro Anda adalah intdan Anda constexpr unsignedadalah unsigned, ada perbedaan penting dan macro hanya memiliki satu keuntungan.

Cakupan

Makro didefinisikan oleh preprocessor dan hanya diganti ke dalam kode setiap kali itu terjadi. Preprocessor bodoh dan tidak memahami sintaks atau semantik C ++. Makro mengabaikan cakupan seperti ruang nama, kelas, atau blok fungsi, sehingga Anda tidak dapat menggunakan nama untuk hal lain dalam file sumber. Itu tidak benar untuk konstanta yang didefinisikan sebagai variabel C ++ yang tepat:

#define MAX_HEIGHT 720
constexpr int max_height = 720;

class Window {
  // ...
  int max_height;
};

Tidak masalah untuk memanggil variabel anggota max_heightkarena ini adalah anggota kelas dan memiliki cakupan yang berbeda, dan berbeda dari yang ada di ruang lingkup namespace. Jika Anda mencoba menggunakan kembali nama MAX_HEIGHTanggota tersebut, maka preprocessor akan mengubahnya menjadi omong kosong yang tidak dapat dikompilasi:

class Window {
  // ...
  int 720;
};

Inilah sebabnya mengapa Anda harus memberikan makro UGLY_SHOUTY_NAMESuntuk memastikannya menonjol dan Anda dapat berhati-hati dalam menamainya untuk menghindari bentrokan. Jika Anda tidak menggunakan makro secara tidak perlu, Anda tidak perlu khawatir tentang itu (dan tidak perlu membaca SHOUTY_NAMES).

Jika Anda hanya menginginkan konstanta di dalam fungsi, Anda tidak dapat melakukannya dengan makro, karena preprocessor tidak tahu apa itu fungsi atau apa artinya berada di dalamnya. Untuk membatasi makro hanya ke bagian tertentu dari file, Anda memerlukannya #undeflagi:

int limit(int height) {
#define MAX_HEIGHT 720
  return std::max(height, MAX_HEIGHT);
#undef MAX_HEIGHT
}

Bandingkan dengan yang jauh lebih masuk akal:

int limit(int height) {
  constexpr int max_height = 720;
  return std::max(height, max_height);
}

Mengapa Anda lebih memilih yang makro?

Lokasi memori yang nyata

Variabel constexpr adalah variabel sehingga sebenarnya ada dalam program dan Anda dapat melakukan hal-hal C ++ normal seperti mengambil alamatnya dan mengikat referensi ke sana.

Kode ini memiliki perilaku yang tidak ditentukan:

#define MAX_HEIGHT 720
int limit(int height) {
  const int& h = std::max(height, MAX_HEIGHT);
  // ...
  return h;
}

Masalahnya adalah itu MAX_HEIGHTbukan variabel, jadi untuk panggilan std::maxsementara intharus dibuat oleh kompilator. Referensi yang dikembalikan oleh std::maxmungkin kemudian merujuk ke sementara itu, yang tidak ada setelah akhir pernyataan itu, jadi return hmengakses memori yang tidak valid.

Masalah itu tidak ada dengan variabel yang tepat, karena memiliki lokasi tetap dalam memori yang tidak hilang:

int limit(int height) {
  constexpr int max_height = 720;
  const int& h = std::max(height, max_height);
  // ...
  return h;
}

(Dalam praktiknya Anda mungkin akan menyatakan int htidak, const int& htetapi masalahnya dapat muncul dalam konteks yang lebih halus.)

Kondisi praprosesor

Satu-satunya waktu untuk memilih makro adalah saat Anda ingin nilainya dipahami oleh preprocessor, untuk digunakan dalam #ifkondisi, mis.

#define MAX_HEIGHT 720
#if MAX_HEIGHT < 256
using height_type = unsigned char;
#else
using height_type = unsigned int;
#endif

Anda tidak dapat menggunakan variabel di sini, karena preprocessor tidak memahami cara merujuk ke variabel dengan nama. Ini hanya memahami hal-hal dasar yang sangat mendasar seperti ekspansi makro dan arahan yang dimulai dengan #(suka #includedan #definedan #if).

Jika anda menginginkan sebuah konstanta yang dapat dipahami oleh preprocessor maka anda harus menggunakan preprocessor untuk mendefinisikannya. Jika Anda menginginkan konstanta untuk kode C ++ normal, gunakan kode C ++ normal.

Contoh di atas hanya untuk mendemonstrasikan kondisi preprocessor, tetapi bahkan kode tersebut dapat menghindari penggunaan preprocessor:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;
Jonathan Wakely
sumber
3
Sebuah constexprvariabel tidak perlu menempati memori sampai alamatnya (pointer / referensi) diambil; jika tidak, itu dapat dioptimalkan sepenuhnya (dan saya pikir mungkin ada Standardese yang menjamin itu). Saya ingin menekankan hal ini agar orang tidak terus menggunakan ' enumretasan' lama yang lebih rendah dari gagasan yang salah arah bahwa hal sepele constexpryang tidak memerlukan penyimpanan akan tetap menempati sebagian.
underscore_d
3
Bagian "A real memory location" Anda salah: 1. Anda mengembalikan dengan value (int), jadi salinan dibuat, sementara tidak menjadi masalah. 2. Seandainya Anda mengembalikan dengan referensi (int &), maka masalah Anda int heightakan sama seperti makro, karena cakupannya terkait dengan fungsi, pada dasarnya juga sementara. 3. Komentar di atas, "const int & h akan memperpanjang masa hidup sementara" sudah benar.
PoweredByRice
4
@underscore_d true, tapi itu tidak mengubah argumen. Variabel tidak memerlukan penyimpanan kecuali ada penggunaan odr-nya. Intinya adalah ketika variabel nyata dengan penyimpanan diperlukan, variabel constexpr melakukan hal yang benar.
Jonathan Wakely
1
@PoweredByRice 1. masalah tidak ada hubungannya dengan nilai kembali limit, masalahnya adalah nilai kembali std::max. 2. ya, itu sebabnya tidak mengembalikan referensi. 3. salah lihat link coliru diatas.
Jonathan Wakely
3
@PoweredByRice menghela napas, Anda benar-benar tidak perlu menjelaskan cara kerja C ++ kepada saya. Jika Anda memiliki const int& h = max(x, y);dan maxmengembalikan dengan nilai seumur hidup nilai pengembaliannya diperpanjang. Bukan oleh tipe kembalian, tapi oleh tipe const int&itu terikat. Apa yang saya tulis benar.
Jonathan Wakely
11

Secara umum, Anda harus menggunakan constexprkapan pun Anda bisa, dan makro hanya jika tidak ada solusi lain yang memungkinkan.

Alasan:

Makro adalah pengganti sederhana dalam kode, dan karena alasan ini, maxmakro sering kali menimbulkan konflik (misalnya makro windows.h vs std::max). Selain itu, makro yang berfungsi dapat dengan mudah digunakan dengan cara berbeda yang kemudian dapat memicu kesalahan kompilasi yang aneh. (misalnya Q_PROPERTYdigunakan pada anggota struktur)

Karena semua ketidakpastian tersebut, merupakan gaya kode yang baik untuk menghindari makro, persis seperti biasanya Anda menghindari gotos.

constexpr didefinisikan secara semantik, dan dengan demikian biasanya menghasilkan masalah yang jauh lebih sedikit.

Adrian Maire
sumber
1
Dalam kasus apa menggunakan makro tidak dapat dihindari?
Tom Dorone
3
Kompilasi bersyarat menggunakan #ifhal-hal yang sebenarnya berguna bagi preprocessor. Mendefinisikan sebuah konstanta bukanlah salah satu hal yang berguna untuk preprocessor, kecuali konstanta itu harus berupa makro karena digunakan dalam kondisi preprocessor menggunakan #if. Jika konstanta digunakan dalam kode C ++ normal (bukan arahan preprocessor), maka gunakan variabel C ++ normal, bukan makro preprocessor.
Jonathan Wakely
Kecuali menggunakan makro variadic, sebagian besar penggunaan makro untuk sakelar kompilator, tetapi mencoba mengganti pernyataan makro saat ini (seperti sakelar bersyarat, string literal) berurusan dengan pernyataan kode nyata dengan constexpr adalah ide yang bagus?
Saya akan mengatakan switch kompiler bukanlah ide yang baik juga. Namun, saya memahami sepenuhnya bahwa ini diperlukan beberapa kali (juga makro), terutama yang berhubungan dengan kode lintas platform atau yang disematkan. Untuk menjawab pertanyaan Anda: Jika Anda sudah berurusan dengan preprocessor, saya akan menggunakan makro agar tetap jelas dan intuitif apa itu preprocessor dan apa itu waktu kompilasi. Saya juga menyarankan untuk memberi komentar yang berat dan membuatnya digunakan sesingkat dan sesingkat mungkin (hindari makro menyebar atau 100 baris #if). Mungkin pengecualiannya adalah penjaga #ifndef yang khas (standar untuk #pragma sekali) yang dipahami dengan baik.
Adrian Maire
3

Jawaban bagus dari Jonathon Wakely . Saya juga menyarankan Anda untuk melihat jawaban jogojapan tentang apa perbedaan antara constdan constexprbahkan sebelum Anda mempertimbangkan penggunaan makro.

Makro itu bodoh, tetapi dalam arti yang baik . Saat ini seolah-olah mereka adalah bantuan pembangunan ketika Anda ingin bagian yang sangat spesifik dari kode Anda hanya dikompilasi dengan adanya parameter build tertentu yang "ditentukan". Biasanya, semua yang berarti mengambil nama makro Anda, atau lebih baik lagi, mari kita sebut saja Trigger, dan menambahkan hal-hal seperti, /D:Trigger, -DTrigger, dll untuk alat membangun yang digunakan.

Meskipun ada banyak kegunaan yang berbeda untuk makro, ini adalah dua yang paling sering saya lihat yang bukan praktik yang buruk / ketinggalan zaman:

  1. Bagian kode khusus Perangkat Keras dan Platform
  2. Peningkatan verbositas membangun

Jadi sementara Anda dapat dalam kasus OP mencapai tujuan yang sama untuk mendefinisikan int dengan constexpratau a MACRO, tidak mungkin keduanya akan tumpang tindih saat menggunakan konvensi modern. Berikut beberapa penggunaan makro umum yang belum dihentikan secara bertahap.

#if defined VERBOSE || defined DEBUG || defined MSG_ALL
    // Verbose message-handling code here
#endif

Sebagai contoh lain untuk penggunaan makro, katakanlah Anda memiliki beberapa perangkat keras yang akan datang untuk dirilis, atau mungkin generasi tertentu darinya yang memiliki beberapa solusi rumit yang tidak diperlukan yang lain. Kami akan mendefinisikan makro ini sebagai GEN_3_HW.

#if defined GEN_3_HW && defined _WIN64
    // Windows-only special handling for 64-bit upcoming hardware
#elif defined GEN_3_HW && defined __APPLE__
    // Special handling for macs on the new hardware
#elif !defined _WIN32 && !defined __linux__ && !defined __APPLE__ && !defined __ANDROID__ && !defined __unix__ && !defined __arm__
    // Greetings, Outlander! ;)
#else
    // Generic handling
#endif
kayleeFrye_onDeck
sumber