Liệu biến constexpr tĩnh bên trong một hàm có ý nghĩa?


192

Nếu tôi có một biến bên trong một hàm (giả sử, một mảng lớn), việc khai báo cả hai staticvà có hợp lý constexprkhông? constexprđảm bảo rằng mảng được tạo ra tại thời điểm biên dịch, vì vậy sẽ staticvô dụng?

void f() {
    static constexpr int x [] = {
        // a few thousand elements
    };
    // do something with the array
}

staticthực sự làm bất cứ điều gì ở đó về mã được tạo ra hoặc ngữ nghĩa?

Câu trả lời:


230

Câu trả lời ngắn gọn là không chỉ statichữu ích, nó còn luôn được mong muốn.

Đầu tiên, lưu ý rằng staticconstexprhoàn toàn độc lập với nhau. staticxác định tuổi thọ của đối tượng trong khi thực hiện; constexprxác định rằng đối tượng nên có sẵn trong quá trình biên dịch. Biên dịch và thực hiện là rời rạc và rời rạc, cả về thời gian và không gian. Vì vậy, một khi chương trình được biên dịch, constexprkhông còn phù hợp.

Mỗi biến tuyên bố constexprlà ngầm constnhưng conststaticgần như trực giao (trừ sự tương tác với static constsố nguyên.)

Các C++mô hình đối tượng (§1.9) yêu cầu tất cả các đối tượng khác hơn chút-lĩnh vực chiếm ít nhất một byte của bộ nhớ và có địa chỉ; hơn nữa tất cả các đối tượng như vậy có thể quan sát được trong một chương trình tại một thời điểm nhất định phải có địa chỉ riêng biệt (đoạn 6). Điều này không hoàn toàn yêu cầu trình biên dịch tạo một mảng mới trên ngăn xếp cho mỗi lần gọi hàm với một mảng const không tĩnh cục bộ, bởi vì trình biên dịch có thể ẩn náu theo as-ifnguyên tắc miễn là nó có thể chứng minh rằng không có đối tượng nào khác có thể như vậy Được Quan sát.

Thật không may, điều đó sẽ không dễ để chứng minh, trừ khi hàm này là tầm thường (ví dụ, nó không gọi bất kỳ hàm nào khác mà phần thân không thể nhìn thấy trong đơn vị dịch) bởi vì các mảng, ít nhiều theo định nghĩa, là các địa chỉ. Vì vậy, trong hầu hết các trường hợp, const(expr)mảng không tĩnh sẽ phải được tạo lại trên ngăn xếp ở mỗi lần gọi, điều này đánh bại điểm có thể tính toán nó trong thời gian biên dịch.

Mặt khác, một static constđối tượng cục bộ được chia sẻ bởi tất cả các nhà quan sát và hơn nữa có thể được khởi tạo ngay cả khi chức năng mà nó được định nghĩa không bao giờ được gọi. Vì vậy, không có điều nào ở trên áp dụng, và một trình biên dịch là miễn phí không chỉ tạo ra một thể hiện duy nhất của nó; nó là miễn phí để tạo một phiên bản duy nhất của nó trong lưu trữ chỉ đọc.

Vì vậy, bạn chắc chắn nên sử dụng static constexprtrong ví dụ của bạn.

Tuy nhiên, có một trường hợp bạn không muốn sử dụng static constexpr. Trừ khi một constexprđối tượng được khai báo là ODR được sử dụng hoặc được khai báo static, trình biên dịch hoàn toàn không bao gồm nó. Điều đó khá hữu ích, vì nó cho phép sử dụng các constexprmảng tạm thời thời gian biên dịch mà không làm ô nhiễm chương trình được biên dịch với các byte không cần thiết. Trong trường hợp đó, rõ ràng bạn sẽ không muốn sử dụng static, vì staticcó khả năng buộc đối tượng tồn tại trong thời gian chạy.


2
@AndrewLazarus, bạn không thể ném ra constkhỏi một constvật thể, chỉ từ một vật const X*chỉ vào một vật X. Nhưng đó không phải là vấn đề; điểm quan trọng là các đối tượng tự động không thể có địa chỉ tĩnh. Như tôi đã nói, constexprkhông còn có ý nghĩa khi biên soạn xong, vì vậy không có gì để cast đi là (và hoàn toàn có thể không có gì cả, vì đối tượng thậm chí còn không được bảo đảm để tồn tại trong thời gian chạy.)
rici

17
Tôi cảm thấy như không chỉ câu trả lời này vô cùng khó hiểu mà còn tự mâu thuẫn. Ví dụ, bạn nói rằng bạn hầu như luôn muốn staticconstexprnhưng giải thích rằng chúng là trực giao và độc lập, làm những việc khác nhau. Sau đó, bạn đề cập đến một lý do KHÔNG kết hợp cả hai vì nó sẽ bỏ qua việc sử dụng ODR (có vẻ hữu ích). Ồ và tôi vẫn không thấy lý do tại sao nên sử dụng tĩnh với constexpr vì tĩnh là dành cho công cụ thời gian chạy. Bạn không bao giờ giải thích tại sao tĩnh với constexpr là quan trọng.
void.pulum

2
@ void.pulum: Bạn nói đúng về đoạn cuối. Tôi đã thay đổi phần giới thiệu. Tôi nghĩ rằng tôi đã giải thích tầm quan trọng của static constexpr(nó ngăn mảng không đổi phải được tạo lại trên mỗi lệnh gọi hàm), nhưng tôi đã điều chỉnh một số từ có thể làm cho nó rõ ràng hơn. Cảm ơn.
rici

8
Cũng có thể hữu ích để đề cập đến hằng số thời gian biên dịch so với hằng thời gian chạy. Nói cách khác, nếu một constexprbiến không đổi chỉ được sử dụng trong bối cảnh thời gian biên dịch và không bao giờ cần thiết trong thời gian chạy, thì sẽ staticvô nghĩa, vì đến thời điểm bạn nhận được vào thời gian chạy, giá trị đã được "nội tuyến" một cách hiệu quả. Tuy nhiên, nếu constexprđược sử dụng trong bối cảnh thời gian chạy (nói cách khác, constexprthì cần phải được chuyển đổi thành constngầm định và có sẵn một địa chỉ vật lý cho mã thời gian chạy) static, ít nhất đó là sự hiểu biết của tôi.
void.pulum

3
Một ví dụ cho nhận xét cuối cùng của tôi : static constexpr int foo = 100;. Không có lý do tại sao trình biên dịch không thể thay thế việc sử dụng fooở mọi nơi cho nghĩa đen 100, trừ khi mã đang làm một cái gì đó như thế &foo. Vì vậy, statictrên fookhông có tính hữu dụng trong trường hợp này vì fookhông tồn tại trong thời gian chạy. Một lần nữa tất cả lên đến trình biên dịch.
void.pulum

10

Ngoài câu trả lời đã cho, cần lưu ý rằng trình biên dịch không bắt buộc phải khởi tạo constexprbiến tại thời gian biên dịch, biết rằng sự khác biệt giữa constexprstatic constexprđó là sử dụng static constexprbạn đảm bảo biến chỉ được khởi tạo một lần.

Mã sau đây cho thấy cách constexprbiến được khởi tạo nhiều lần (với cùng một giá trị), trong khi static constexprchắc chắn chỉ được khởi tạo một lần.

Ngoài ra, mã so sánh lợi thế constexprso với constkết hợp với static.

#include <iostream>
#include <string>
#include <cassert>
#include <sstream>

const short const_short = 0;
constexpr short constexpr_short = 0;

// print only last 3 address value numbers
const short addr_offset = 3;

// This function will print name, value and address for given parameter
void print_properties(std::string ref_name, const short* param, short offset)
{
    // determine initial size of strings
    std::string title = "value \\ address of ";
    const size_t ref_size = ref_name.size();
    const size_t title_size = title.size();
    assert(title_size > ref_size);

    // create title (resize)
    title.append(ref_name);
    title.append(" is ");
    title.append(title_size - ref_size, ' ');

    // extract last 'offset' values from address
    std::stringstream addr;
    addr << param;
    const std::string addr_str = addr.str();
    const size_t addr_size = addr_str.size();
    assert(addr_size - offset > 0);

    // print title / ref value / address at offset
    std::cout << title << *param << " " << addr_str.substr(addr_size - offset) << std::endl;
}

// here we test initialization of const variable (runtime)
void const_value(const short counter)
{
    static short temp = const_short;
    const short const_var = ++temp;
    print_properties("const", &const_var, addr_offset);

    if (counter)
        const_value(counter - 1);
}

// here we test initialization of static variable (runtime)
void static_value(const short counter)
{
    static short temp = const_short;
    static short static_var = ++temp;
    print_properties("static", &static_var, addr_offset);

    if (counter)
        static_value(counter - 1);
}

// here we test initialization of static const variable (runtime)
void static_const_value(const short counter)
{
    static short temp = const_short;
    static const short static_var = ++temp;
    print_properties("static const", &static_var, addr_offset);

    if (counter)
        static_const_value(counter - 1);
}

// here we test initialization of constexpr variable (compile time)
void constexpr_value(const short counter)
{
    constexpr short constexpr_var = constexpr_short;
    print_properties("constexpr", &constexpr_var, addr_offset);

    if (counter)
        constexpr_value(counter - 1);
}

// here we test initialization of static constexpr variable (compile time)
void static_constexpr_value(const short counter)
{
    static constexpr short static_constexpr_var = constexpr_short;
    print_properties("static constexpr", &static_constexpr_var, addr_offset);

    if (counter)
        static_constexpr_value(counter - 1);
}

// final test call this method from main()
void test_static_const()
{
    constexpr short counter = 2;

    const_value(counter);
    std::cout << std::endl;

    static_value(counter);
    std::cout << std::endl;

    static_const_value(counter);
    std::cout << std::endl;

    constexpr_value(counter);
    std::cout << std::endl;

    static_constexpr_value(counter);
    std::cout << std::endl;
}

Đầu ra chương trình có thể:

value \ address of const is               1 564
value \ address of const is               2 3D4
value \ address of const is               3 244

value \ address of static is              1 C58
value \ address of static is              1 C58
value \ address of static is              1 C58

value \ address of static const is        1 C64
value \ address of static const is        1 C64
value \ address of static const is        1 C64

value \ address of constexpr is           0 564
value \ address of constexpr is           0 3D4
value \ address of constexpr is           0 244

value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0

Như bạn có thể thấy bản thân constexprđược statickhởi tạo nhiều lần (địa chỉ không giống nhau) trong khi từ khóa đảm bảo rằng việc khởi tạo chỉ được thực hiện một lần.


chúng ta không thể sử dụng constexpr const short constexpr_shortđể đưa ra lỗi nếu constexpr_short được khởi tạo lại
akhileshzmishra

cú pháp của constexpr constbạn không có ý nghĩa gì vì constexprđã có const, việc thêm constmột hoặc nhiều lần sẽ bị trình biên dịch bỏ qua. Bạn đang cố bắt lỗi nhưng đây không phải là lỗi, đó là cách mà hầu hết các trình biên dịch hoạt động.
metottaster
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.