Constexpr so với macro


92

Tôi nên sử dụng macro ở đâu và tôi thích constexpr ở đâu? Về cơ bản chúng không giống nhau sao?

#define MAX_HEIGHT 720

vs

constexpr unsigned int max_height = 720;

4
AFAIK constexpr cung cấp nhiều loại an toàn hơn
Code-Apprentice

13
Easy: constexr, luôn luôn.
n. 'đại từ' m.

Có thể trả lời một số câu hỏi của bạn stackoverflow.com/q/4748083/540286
Ortwin Angermeier

Câu trả lời:


147

Về cơ bản chúng không giống nhau sao?

Không. Hoàn toàn không. Thậm chí không gần.

Ngoài thực tế macro của bạn là một intvà của bạn constexpr unsignedlà một unsigned, có những khác biệt quan trọng và macro chỉ có một lợi thế.

Phạm vi

Macro được xác định bởi bộ tiền xử lý và được thay thế đơn giản vào mã mỗi khi nó xảy ra. Bộ tiền xử lý ngu ngốc và không hiểu cú pháp hoặc ngữ nghĩa C ++. Macro bỏ qua các phạm vi như không gian tên, lớp hoặc khối chức năng, vì vậy bạn không thể sử dụng tên cho bất kỳ thứ gì khác trong tệp nguồn. Điều đó không đúng đối với một hằng được xác định là một biến C ++ thích hợp:

#define MAX_HEIGHT 720
constexpr int max_height = 720;

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

Sẽ tốt khi có một biến thành viên được gọi max_heightbởi vì nó là một thành viên của lớp và do đó có một phạm vi khác và khác biệt với một trong phạm vi không gian tên. Nếu bạn đã cố gắng sử dụng lại tên MAX_HEIGHTcho thành viên thì bộ xử lý tiền xử lý sẽ thay đổi nó thành vô nghĩa này sẽ không biên dịch:

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

Đây là lý do tại sao bạn phải đặt macro UGLY_SHOUTY_NAMESđể đảm bảo chúng nổi bật và bạn có thể cẩn thận trong việc đặt tên chúng để tránh đụng độ. Nếu bạn không sử dụng macro một cách không cần thiết, bạn không phải lo lắng về điều đó (và không cần phải đọc SHOUTY_NAMES).

Nếu bạn chỉ muốn một hằng số bên trong một hàm, bạn không thể làm điều đó với macro, bởi vì bộ tiền xử lý không biết hàm là gì hoặc ý nghĩa của nó ở bên trong nó. Để giới hạn macro chỉ ở một phần nhất định của tệp, bạn cần #undeflại macro :

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

So sánh với điều hợp lý hơn nhiều:

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

Tại sao bạn thích macro hơn?

Một vị trí bộ nhớ thực sự

Biến constexpr là một biến để nó thực sự tồn tại trong chương trình và bạn có thể làm những việc C ++ bình thường như lấy địa chỉ của nó và liên kết một tham chiếu đến nó.

Mã này có hành vi không xác định:

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

Vấn đề là đó MAX_HEIGHTkhông phải là một biến, vì vậy để gọi std::maxmột tạm thời intphải được tạo bởi trình biên dịch. Tham chiếu được trả về std::maxsau đó có thể tham chiếu đến tạm thời đó, không tồn tại sau khi kết thúc câu lệnh đó, do đó return htruy cập bộ nhớ không hợp lệ.

Vấn đề đó chỉ đơn giản là không tồn tại với một biến thích hợp, vì nó có một vị trí cố định trong bộ nhớ mà không biến mất:

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

(Trong thực tế, bạn có thể tuyên bố là int hkhông const int& hnhưng vấn đề có thể phát sinh trong những bối cảnh phức tạp hơn.)

Điều kiện tiền xử lý

Thời điểm duy nhất để thích macro là khi bạn cần bộ tiền xử lý hiểu giá trị của nó, để sử dụng trong các #ifđiều kiện, ví dụ:

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

Bạn không thể sử dụng một biến ở đây, vì bộ tiền xử lý không hiểu cách tham chiếu đến các biến theo tên. Nó chỉ hiểu những thứ cơ bản rất cơ bản như mở rộng macro và các chỉ thị bắt đầu bằng #(như #include#define#if).

Nếu bạn muốn một hằng số có thể được hiểu bởi bộ tiền xử lý thì bạn nên sử dụng bộ tiền xử lý để định nghĩa nó. Nếu bạn muốn một hằng số cho mã C ++ bình thường, hãy sử dụng mã C ++ bình thường.

Ví dụ trên chỉ để chứng minh điều kiện của bộ xử lý trước, nhưng ngay cả đoạn mã đó cũng có thể tránh sử dụng bộ tiền xử lý:

using height_type = std::conditional_t<max_height < 256, unsigned char, unsigned int>;

3
Một constexprbiến không cần chiếm bộ nhớ cho đến khi địa chỉ của nó (một con trỏ / tham chiếu) được lấy; nếu không, nó có thể được tối ưu hóa hoàn toàn (và tôi nghĩ có thể có Standardese đảm bảo điều đó). Tôi muốn nhấn mạnh điều này để mọi người không tiếp tục sử dụng 'bản enumhack' cũ kỹ, kém chất lượng vì một ý tưởng sai lầm rằng một thứ tầm thường constexprkhông yêu cầu lưu trữ vẫn sẽ chiếm một số.
underscore_d

3
Phần "Vị trí bộ nhớ thực" của bạn sai: 1. Bạn đang trả về theo giá trị (int), vì vậy một bản sao sẽ được tạo, tạm thời không phải là vấn đề. 2. Nếu bạn trả về bằng tham chiếu (int &), thì int heightvấn đề của bạn sẽ giống như macro, vì phạm vi của nó được gắn với hàm, về cơ bản cũng tạm thời. 3. Nhận xét ở trên, "const int & h sẽ kéo dài thời gian tồn tại của tạm thời" là đúng.
PoweredByRice

4
@underscore_d true, nhưng điều đó không thay đổi đối số. Biến sẽ không yêu cầu bộ nhớ trừ khi có cách sử dụng nó. Vấn đề là khi một biến thực có bộ nhớ được yêu cầu, thì biến constexpr sẽ làm đúng.
Jonathan Wakely

1
@PoweredByRice 1. vấn đề không liên quan gì đến giá trị trả về của limit, vấn đề là giá trị trả về của std::max. 2. vâng, đó là lý do tại sao nó không trả về một tham chiếu. 3. sai, xem liên kết coliru ở trên.
Jonathan Wakely

3
@PoweredByRice thở dài, bạn thực sự không cần phải giải thích cách hoạt động của C ++ với tôi. Nếu bạn có const int& h = max(x, y);maxtrả về bằng giá trị thì thời gian tồn tại của giá trị trả về sẽ được kéo dài. Không phải bởi kiểu trả về, mà bởi kiểu const int&nó bị ràng buộc. Những gì tôi đã viết là chính xác.
Jonathan Wakely

11

Nói chung, bạn nên sử dụng constexprbất cứ khi nào bạn có thể và chỉ macro nếu không có giải pháp nào khác.

Cơ sở lý luận:

Macro là một sự thay thế đơn giản trong mã và vì lý do này, chúng thường tạo ra xung đột (ví dụ: maxmacro windows.h vs std::max). Ngoài ra, một macro hoạt động có thể dễ dàng được sử dụng theo cách khác, sau đó có thể gây ra các lỗi biên dịch lạ. (ví dụ: Q_PROPERTYđược sử dụng trên các thành viên cấu trúc)

Do tất cả những điều không chắc chắn đó, cách tốt nhất là tránh macro, giống như cách bạn thường tránh gotos.

constexpr được định nghĩa về mặt ngữ nghĩa, và do đó thường tạo ra ít vấn đề hơn.


1
Trong trường hợp nào thì việc sử dụng macro không thể tránh khỏi?
Tom Dorone

3
Biên dịch có điều kiện sử dụng #iftức là những thứ mà bộ tiền xử lý thực sự hữu ích. Định nghĩa một hằng số không phải là một trong những điều mà bộ tiền xử lý hữu ích, trừ khi hằng số đó phải là một macro vì nó được sử dụng trong các điều kiện sử dụng bộ tiền xử lý #if. Nếu hằng được sử dụng trong mã C ++ bình thường (không phải chỉ thị tiền xử lý) thì hãy sử dụng biến C ++ bình thường, không phải macro tiền xử lý.
Jonathan Wakely

Ngoại trừ việc sử dụng macro đa dạng, chủ yếu sử dụng macro cho các công tắc trình biên dịch, nhưng cố gắng thay thế các câu lệnh macro hiện tại (chẳng hạn như các công tắc có điều kiện, chuỗi ký tự) bằng cách xử lý các câu lệnh mã thực với constexpr có phải là một ý kiến ​​hay không?

Tôi sẽ nói rằng công tắc trình biên dịch cũng không phải là một ý tưởng hay. Tuy nhiên, tôi hoàn toàn hiểu nó là cần thiết đôi khi (cũng là macro), đặc biệt là xử lý mã đa nền tảng hoặc mã nhúng. Để trả lời câu hỏi của bạn: Nếu bạn đang xử lý tiền xử lý, tôi sẽ sử dụng macro để giữ rõ ràng và trực quan tiền xử lý là gì và thời gian biên dịch là gì. Tôi cũng khuyên bạn nên bình luận nhiều và sử dụng nó càng ngắn gọn và cục bộ càng tốt (tránh macro lan rộng ra xung quanh hoặc 100 dòng #if). Có thể ngoại lệ là trình bảo vệ #ifndef điển hình (tiêu chuẩn cho #pragma một lần) được hiểu rõ.
Adrian Maire

3

Câu trả lời tuyệt vời của Jonathon Wakely . Tôi cũng khuyên bạn nên xem câu trả lời của jogojapan về sự khác biệt giữa constconstexprtrước khi bạn đi xem xét việc sử dụng macro.

Macro là ngu ngốc, nhưng theo một cách tốt . Rõ ràng ngày nay chúng là một công cụ hỗ trợ khi bạn muốn các phần rất cụ thể trong mã của mình chỉ được biên dịch khi có một số tham số xây dựng nhất định được "xác định". Thông thường, tất cả những gì có nghĩa là đang diễn tên macro của bạn, hoặc tốt hơn nữa, chúng ta hãy gọi nó là một Trigger, và những thứ thêm thích, /D:Trigger, -DTrigger, vv để các công cụ xây dựng được sử dụng.

Mặc dù có nhiều cách sử dụng khác nhau cho macro, nhưng đây là hai cách tôi thấy thường xuyên nhất mà không phải là cách làm xấu / lỗi thời:

  1. Phần mã dành riêng cho phần cứng và nền tảng
  2. Tăng độ dài bản dựng

Vì vậy, mặc dù trong trường hợp của OP, bạn có thể thực hiện cùng một mục tiêu là xác định số nguyên có constexprhoặc a MACRO, nhưng không chắc cả hai sẽ có sự chồng chéo khi sử dụng các quy ước hiện đại. Dưới đây là một số cách sử dụng macro phổ biến vẫn chưa bị loại bỏ.

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

Một ví dụ khác cho việc sử dụng macro, giả sử bạn có một số phần cứng sắp phát hành hoặc có thể là một thế hệ cụ thể của nó có một số cách giải quyết phức tạp mà những phần cứng khác không yêu cầu. Chúng tôi sẽ xác định macro này là 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.