Cách thành ngữ để phân biệt hai hàm tạo zero-arg


41

Tôi có một lớp học như thế này:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Thông thường tôi muốn mặc định (không) khởi tạo countsmảng như được hiển thị.

Tuy nhiên, tại các vị trí đã chọn được xác định bằng cách định hình, tôi muốn loại bỏ việc khởi tạo mảng, vì tôi biết mảng sắp bị ghi đè, nhưng trình biên dịch không đủ thông minh để tìm ra nó.

Một cách thành ngữ và hiệu quả để tạo ra một hàm tạo zero-arg "thứ cấp" như vậy là gì?

Hiện tại, tôi đang sử dụng một lớp thẻ uninit_tagđược truyền dưới dạng đối số giả, như vậy:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Sau đó, tôi gọi hàm tạo không có init như event_counts c(uninit_tag{});khi tôi muốn ngăn chặn việc xây dựng.

Tôi mở cho các giải pháp không liên quan đến việc tạo ra một lớp giả, hoặc hiệu quả hơn theo một cách nào đó, v.v.


"bởi vì tôi biết mảng sắp bị ghi đè" Bạn có chắc chắn 100% trình biên dịch của bạn chưa thực hiện tối ưu hóa cho bạn không? trường hợp tại điểm: gcc.godbolt.org/z/bJnAuJ
Frank

6
@Frank - Tôi cảm thấy câu trả lời cho câu hỏi của bạn nằm ở nửa sau của câu bạn trích dẫn? Nó không thuộc về câu hỏi, nhưng nhiều điều có thể xảy ra: (a) thường trình biên dịch đơn giản là không đủ mạnh để loại bỏ các cửa hàng chết (b) đôi khi chỉ một tập hợp con của các phần tử được ghi đè và điều này đánh bại tối ưu hóa (nhưng chỉ có cùng một tập hợp con được đọc sau) (c) đôi khi trình biên dịch có thể làm điều đó, nhưng bị đánh bại, ví dụ, bởi vì phương thức không được nội tuyến.
BeeOnRope

Bạn có bất kỳ nhà xây dựng khác trong lớp học của bạn?
NathanOliver

1
@Frank - eh, trường hợp của bạn cho thấy gcc không loại bỏ các cửa hàng chết? Trong thực tế, nếu bạn đã làm cho tôi đoán tôi đã nghĩ rằng gcc sẽ giải quyết trường hợp rất đơn giản này, nhưng nếu nó thất bại ở đây thì hãy tưởng tượng bất kỳ trường hợp phức tạp hơn một chút!
BeeOnRope

1
@uneven_mark - vâng, gcc 9.2 thực hiện ở -O3 (nhưng tối ưu hóa này không phổ biến so với -O2, IME), nhưng các phiên bản trước đó thì không. Nói chung, loại bỏ cửa hàng chết là một điều, nhưng nó rất mong manh và tuân theo tất cả các cảnh báo thông thường, chẳng hạn như trình biên dịch có thể nhìn thấy các cửa hàng chết cùng lúc nó nhìn thấy các cửa hàng thống trị. Nhận xét của tôi là nhiều hơn để làm rõ những gì Frank đã cố gắng nói vì anh ấy nói "trường hợp tại điểm: (liên kết godbolt)" nhưng liên kết cho thấy cả hai cửa hàng đang được thực hiện (vì vậy có lẽ tôi đang thiếu một cái gì đó).
BeeOnRope

Câu trả lời:


33

Giải pháp bạn đã có là chính xác và chính xác là những gì tôi muốn xem nếu tôi đang xem lại mã của bạn. Đó là hiệu quả nhất có thể, rõ ràng và súc tích.


1
Vấn đề chính tôi có là liệu tôi có nên tuyên bố một uninit_taghương vị mới ở mọi nơi mà tôi muốn sử dụng thành ngữ này hay không. Tôi đã hy vọng đã có một cái gì đó giống như một loại chỉ báo như vậy, có lẽ trong std::.
BeeOnRope

9
Không có sự lựa chọn rõ ràng từ thư viện tiêu chuẩn. Tôi sẽ không xác định thẻ mới cho mọi lớp nơi tôi muốn tính năng này - Tôi sẽ xác định no_initthẻ toàn dự án và sử dụng thẻ đó trong tất cả các lớp của mình khi cần.
John Zwinck

2
Tôi nghĩ rằng thư viện tiêu chuẩn có các thẻ manly để phân biệt các trình vòng lặp và các công cụ như vậy và hai std::piecewise_construct_tstd::in_place_t. Không ai trong số họ có vẻ hợp lý để sử dụng ở đây. Có thể bạn muốn xác định một đối tượng toàn cầu thuộc loại của mình để sử dụng luôn, vì vậy bạn không cần dấu ngoặc trong mỗi lệnh gọi của hàm tạo. STL thực hiện điều này với std::piecewise_constructcho std::piecewise_construct_t.
n314159

Nó không hiệu quả nhất có thể. Ví dụ, trong quy ước gọi điện của AArch64, thẻ phải được phân bổ theo ngăn xếp, với các hiệu ứng kích hoạt (không thể gọi đuôi ...): godbolt.org/z/6mSsmq
TLW

1
@TLW Khi bạn thêm cơ thể vào các nhà xây dựng, không có phân bổ ngăn xếp, godbolt.org/z/vkCD65
R2RT

8

Nếu phần thân của hàm tạo trống, nó có thể được bỏ qua hoặc mặc định:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

Sau đó, khởi tạo mặc định event_counts counts; sẽ counts.countskhông được khởi tạo (khởi tạo mặc định là không có ở đây) và khởi tạo event_counts counts{}; giá trị sẽ có giá trị khởi tạo counts.counts, lấp đầy nó bằng các số không.


3
Nhưng sau đó, một lần nữa bạn phải nhớ sử dụng khởi tạo giá trị và OP muốn nó được an toàn theo mặc định.
doc

@doc, mình đồng ý. Đây không phải là giải pháp chính xác cho những gì OP muốn. Nhưng khởi tạo này bắt chước các loại tích hợp. Vì int i;chúng tôi chấp nhận rằng nó không phải là khởi tạo. Có lẽ chúng ta cũng nên chấp nhận rằng event_counts counts;nó không được khởi tạo bằng 0 và làm cho event_counts counts{};mặc định mới của chúng ta.
Evg

6

Tôi thích giải pháp của bạn. Bạn cũng có thể đã xem xét cấu trúc và biến tĩnh lồng nhau. Ví dụ:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

Với biến tĩnh, lệnh gọi hàm tạo chưa được khởi tạo có vẻ thuận tiện hơn:

event_counts e(event_counts::uninit);

Tất nhiên bạn có thể giới thiệu một macro để lưu gõ và làm cho nó trở thành một tính năng có hệ thống hơn

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

3

Tôi nghĩ rằng một enum là một lựa chọn tốt hơn so với một lớp thẻ hoặc bool. Bạn không cần phải truyền một thể hiện của một cấu trúc và nó rõ ràng từ người gọi mà bạn đang nhận tùy chọn.

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Sau đó, việc tạo các trường hợp trông như thế này:

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

Hoặc, để làm cho nó giống với cách tiếp cận lớp thẻ hơn, hãy sử dụng một enum giá trị đơn thay vì lớp thẻ:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

Sau đó, chỉ có hai cách để tạo một thể hiện:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

Tôi đồng ý với bạn: enum đơn giản hơn. Nhưng có lẽ bạn đã quên dòng này:event_counts() : counts{} {}
bluish

@bluish, ý định của tôi không phải là khởi tạo countsvô điều kiện, mà chỉ khi INITđược đặt.
TimK

@bluish Tôi nghĩ lý do chính để chọn một lớp thẻ không phải để đạt được sự đơn giản, mà là để báo hiệu rằng đối tượng chưa được khởi tạo là đặc biệt, tức là nó sử dụng tính năng tối ưu hóa chứ không phải là một phần bình thường của giao diện lớp. Cả hai boolenumlà đàng hoàng, nhưng chúng ta phải nhận thức được rằng việc sử dụng tham số thay vì tình trạng quá tải có bóng râm ngữ nghĩa hơi khác. Trước đây, bạn rõ ràng đã tham số hóa một đối tượng, do đó lập trường khởi tạo / chưa khởi tạo trở thành trạng thái của nó, trong khi việc truyền một đối tượng thẻ cho ctor giống như yêu cầu lớp thực hiện chuyển đổi. Vì vậy, đây không phải là vấn đề của sự lựa chọn cú pháp.
doc

@TimK Nhưng OP muốn hành vi mặc định là khởi tạo mảng, vì vậy tôi nghĩ rằng giải pháp cho câu hỏi của bạn nên bao gồm event_counts() : counts{} {}.
xanh lam

@bluish Trong đề xuất ban đầu của tôi countsđược khởi tạo bởi std::filltrừ khi NO_INITđược yêu cầu. Thêm hàm tạo mặc định như bạn đề xuất sẽ tạo ra hai cách khác nhau để thực hiện khởi tạo mặc định, đó không phải là một ý tưởng tuyệt vời. Tôi đã thêm một cách tiếp cận khác mà tránh sử dụng std::fill.
TimK

1

Bạn có thể muốn xem xét khởi tạo hai pha cho lớp của mình:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

Hàm tạo ở trên không khởi tạo mảng thành 0. Để đặt các phần tử của mảng thành 0, bạn phải gọi hàm thành viên set_zero()sau khi xây dựng.


7
Cảm ơn, tôi đã xem xét phương pháp này nhưng muốn một cái gì đó giữ an toàn mặc định - tức là bằng 0 theo mặc định và chỉ ở một vài nơi được chọn, tôi ghi đè hành vi sang trạng thái không an toàn.
BeeOnRope

3
Điều này sẽ đòi hỏi sự chăm sóc thêm để được thực hiện ở tất cả nhưng việc sử dụng được cho là chưa được khởi tạo. Vì vậy, nó là một nguồn bổ sung các lỗi liên quan đến giải pháp OP.
quả óc chó

@BeeOnRope người ta cũng có thể cung cấp std::functiondưới dạng đối số hàm tạo với nội dung tương tự set_zeronhư đối số mặc định. Sau đó, bạn sẽ truyền một hàm lambda nếu bạn muốn mảng chưa được khởi tạo.
doc

1

Tôi sẽ làm như thế này:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Trình biên dịch sẽ đủ thông minh để bỏ qua tất cả các mã khi bạn sử dụng event_counts(false)và bạn có thể nói chính xác ý của bạn thay vì làm cho giao diện lớp của bạn trở nên kỳ lạ.


8
Bạn nói đúng về hiệu quả, nhưng các tham số boolean không tạo ra mã máy khách dễ đọc. Khi bạn đọc và bạn thấy phần khai báo event_counts(false), điều đó có nghĩa là gì? Bạn không có ý tưởng mà không quay lại và nhìn vào tên của tham số. Tốt nhất là sử dụng một enum, hoặc, trong trường hợp này, một lớp sentinel / tag như trong câu hỏi. Sau đó, bạn nhận được một tuyên bố giống như event_counts(no_init), đó là điều hiển nhiên đối với mọi người theo nghĩa của nó.
Cody Grey

Tôi nghĩ rằng đây cũng là giải pháp tốt. Bạn có thể loại bỏ ctor mặc định và sử dụng giá trị mặc định event_counts(bool initCountr = true).
doc

Ngoài ra, ctor nên được rõ ràng.
doc

Thật không may, hiện tại C ++ không hỗ trợ các tham số được đặt tên, nhưng chúng tôi có thể sử dụng boost::parametervà gọi event_counts(initCounts = false)để dễ đọc
phuclv

1
Thật thú vị, @doc, event_counts(bool initCounts = true)thực sự một hàm tạo mặc định, do mọi tham số có giá trị mặc định. Yêu cầu chỉ là nó có thể gọi được mà không chỉ định đối số, event_counts ec;không quan tâm nếu nó không tham số hoặc sử dụng các giá trị mặc định.
Thời gian của Justin - Tái lập lại

1

Tôi sẽ sử dụng một lớp con chỉ để tiết kiệm một chút gõ:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

Bạn có thể thoát khỏi lớp giả bởi sự thay đổi các tham số của các nhà xây dựng không khởi tạo cho boolhay inthoặc một cái gì đó, vì nó không phải là ghi nhớ nữa.

Bạn cũng có thể trao đổi thừa kế xung quanh và xác định events_count_no_initvới một hàm tạo mặc định như Evg đã đề xuất trong câu trả lời của họ và sau đó phải events_countlà lớp con:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

Đây là một ý tưởng thú vị, nhưng tôi cũng cảm thấy muốn giới thiệu một loại mới sẽ gây ra ma sát. Ví dụ, khi tôi thực sự muốn một cái chưa được khởi tạo event_counts, tôi sẽ muốn nó thuộc loại event_countchứ không phải event_count_uninitialized, vì vậy tôi nên cắt ngay khi xây dựng event_counts c = event_counts_no_init{};, điều mà tôi nghĩ là loại bỏ hầu hết các khoản tiết kiệm khi gõ.
BeeOnRope

@BeeOnRope Vâng, đối với hầu hết các mục đích, một event_count_uninitializedđối tượng là một event_countđối tượng. Đó là toàn bộ quan điểm thừa kế, chúng không phải là loại hoàn toàn khác nhau.
Ross Ridge

Đồng ý, nhưng chà là với "cho hầu hết các mục đích". Chúng không thể thay thế cho nhau - ví dụ: nếu bạn cố gắng xem gán ecucho ecnó hoạt động, nhưng không phải là cách khác. Hoặc nếu bạn sử dụng các hàm mẫu, chúng là các loại khác nhau và kết thúc bằng các cảnh báo khác nhau ngay cả khi hành vi kết thúc giống hệt nhau (và đôi khi nó sẽ không là ví dụ, với các thành viên mẫu tĩnh). Đặc biệt với việc sử dụng nhiều thứ autonày chắc chắn có thể mọc lên và gây nhầm lẫn: Tôi không muốn cách một đối tượng được khởi tạo sẽ được phản ánh vĩnh viễn trong loại của nó.
BeeOnRope
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.