Cách thanh lịch nhất để viết một cú 'nếu'


136

Vì C ++ 17, người ta có thể viết một ifkhối sẽ được thực thi chính xác một lần như thế này:

#include <iostream>
int main() {
    for (unsigned i = 0; i < 10; ++i) {

        if (static bool do_once = true; do_once) { // Enter only once
            std::cout << "hello one-shot" << std::endl;
            // Possibly much more code
            do_once = false;
        }

    }
}

Tôi biết tôi có thể đang xem xét lại điều này, và có nhiều cách khác để giải quyết vấn đề này, nhưng vẫn có thể viết điều này bằng cách nào đó như thế này, vì vậy do_once = falsecuối cùng không cần đến nó?

if (DO_ONCE) {
    // Do stuff
}

Tôi đang nghĩ một chức năng của trình trợ giúp do_once(), có chứa static bool do_once, nhưng nếu tôi muốn sử dụng cùng chức năng đó ở những nơi khác nhau thì sao? Đây có thể là thời gian và địa điểm cho một #define? Tôi hy vọng là không.


52
Tại sao không chỉ if (i == 0)? Nó đủ rõ ràng.
SilvanoCerza

26
@SilvanoCerza Vì đó không phải là vấn đề. Khối if này có thể ở đâu đó trong một số chức năng được thực thi nhiều lần, không phải trong một vòng lặp thông thường
nada

8
có thể std::call_oncelà một tùy chọn (nó được sử dụng để xâu chuỗi, nhưng nó vẫn hoạt động).
fdan

25
Ví dụ của bạn có thể là một sự phản ánh xấu về vấn đề trong thế giới thực của bạn mà bạn không chỉ cho chúng tôi, nhưng tại sao không bỏ chức năng gọi một lần ra khỏi vòng lặp?
rubenvb

14
Nó đã không xảy ra với tôi rằng các biến được khởi tạo trong một ifđiều kiện có thể static. Thật khéo léo.
HolyBlackCat

Câu trả lời:


143

Sử dụng std::exchange:

if (static bool do_once = true; std::exchange(do_once, false))

Bạn có thể làm cho nó ngắn hơn đảo ngược giá trị thật:

if (static bool do_once; !std::exchange(do_once, true))

Nhưng nếu bạn đang sử dụng nhiều thứ này, đừng thích và thay vào đó hãy tạo một trình bao bọc:

struct Once {
    bool b = true;
    explicit operator bool() { return std::exchange(b, false); }
};

Và sử dụng nó như:

if (static Once once; once)

Biến không được phép tham chiếu ngoài điều kiện, vì vậy tên không mua cho chúng tôi nhiều. Lấy cảm hứng từ các ngôn ngữ khác như Python mang ý nghĩa đặc biệt cho mã _định danh, chúng tôi có thể viết:

if (static Once _; _)

Cải thiện thêm: tận dụng phần BSS (@Ded repeatator), tránh ghi bộ nhớ khi chúng tôi đã chạy (@ShadowRanger) và đưa ra gợi ý dự đoán chi nhánh nếu bạn sẽ kiểm tra nhiều lần (ví dụ như trong câu hỏi):

// GCC, Clang, icc only; use [[likely]] in C++20 instead
#define likely(x) __builtin_expect(!!(x), 1)

struct Once {
    bool b = false;
    explicit operator bool()
    {
        if (likely(b))
            return false;

        b = true;
        return true;
    }
};

32
Tôi biết các macro nhận được rất nhiều sự ghét bỏ trong C ++ nhưng điều này trông có vẻ rất rõ ràng: #define ONLY_ONCE if (static bool DO_ONCE_ = true; std::exchange(DO_ONCE_, false))Được sử dụng như:ONLY_ONCE { foo(); }

5
ý tôi là, nếu bạn viết "một lần" ba lần, sau đó sử dụng nó nhiều hơn ba lần trong các câu lệnh nếu nó đáng giá như vậy
Alan

13
Tên _được sử dụng trong rất nhiều phần mềm để đánh dấu các chuỗi có thể dịch. Mong đợi những điều thú vị sẽ xảy ra.
Simon Richter

1
Nếu bạn có sự lựa chọn, hãy ưu tiên giá trị ban đầu của trạng thái tĩnh là all-bits-zero. Hầu hết các định dạng thực thi chứa chiều dài của một khu vực hoàn toàn bằng không.
Ded repeatator

7
Sử dụng _cho biến sẽ có un-Pythonic. Bạn không sử dụng _cho các biến sẽ được tham chiếu sau, chỉ để lưu trữ các giá trị mà bạn phải cung cấp một biến nhưng bạn không cần giá trị. Nó thường được sử dụng để giải nén khi bạn chỉ cần một số giá trị. (Có các trường hợp sử dụng khác, nhưng chúng khá khác so với trường hợp giá trị vứt đi.)
jpmc26

91

Có thể không phải là giải pháp tao nhã nhất và bạn không thấy bất kỳ thực tế nào if, nhưng thư viện tiêu chuẩn thực sự bao gồm trường hợp này:, xem std::call_once.

#include <mutex>

std::once_flag flag;

for (int i = 0; i < 10; ++i)
    std::call_once(flag, [](){ std::puts("once\n"); });

Ưu điểm ở đây là đây là chủ đề an toàn.


3
Tôi đã không biết về std :: call_once trong bối cảnh đó. Nhưng với giải pháp này, bạn cần khai báo std :: once_flag cho mọi nơi bạn sử dụng std :: call_once, phải không?
nada

11
Điều này hoạt động, nhưng không có nghĩa là một giải pháp đơn giản, ý tưởng là cho các ứng dụng đa luồng. Đó là quá mức cho một cái gì đó đơn giản bởi vì nó sử dụng đồng bộ hóa nội bộ - tất cả cho một cái gì đó sẽ được giải quyết bằng cách đơn giản nếu. Ông đã không yêu cầu một giải pháp an toàn chủ đề.
Michael Chourdakis

9
@MichaelChourdakis Tôi đồng ý với bạn, đó là một sự quá mức cần thiết. Tuy nhiên, thật đáng để biết và đặc biệt là biết về khả năng diễn đạt những gì bạn đang làm ("thực thi mã này một lần") thay vì che giấu điều gì đó đằng sau một thủ thuật if-trick khó đọc hơn.
Lubgr

17
Uh, nhìn thấy call_oncetôi có nghĩa là bạn muốn gọi một cái gì đó một lần. Điên rồi, tôi biết.
Barry

3
@SergeyA bởi vì nó sử dụng đồng bộ hóa nội bộ - tất cả cho một cái gì đó sẽ được giải quyết bằng một đơn giản nếu. Đó là một cách khá thành ngữ để làm một cái gì đó khác với những gì được yêu cầu.
Các cuộc đua nhẹ nhàng trong quỹ đạo

52

C ++ đã có một luồng nguyên thủy điều khiển dựng sẵn bao gồm "( trước khối; điều kiện; sau khối )":

for (static bool b = true; b; b = false)

Hoặc tin tặc, nhưng ngắn hơn:

for (static bool b; !b; b = !b)

Tuy nhiên, tôi nghĩ rằng bất kỳ kỹ thuật nào được trình bày ở đây nên được sử dụng cẩn thận, vì chúng không (chưa?) Rất phổ biến.


1
Tôi thích tùy chọn đầu tiên (mặc dù - giống như nhiều biến thể ở đây - nó không phải là luồng an toàn, vì vậy hãy cẩn thận). Tùy chọn thứ hai mang lại cho tôi cảm giác ớn lạnh (khó đọc hơn và có thể được thực hiện bất kỳ số lần nào chỉ với hai luồng ... b == false: Thread 1đánh giá !bvà nhập vòng lặp, Thread 2đánh giá !bvà nhập vòng lặp, Thread 1thực hiện công cụ của nó và rời khỏi vòng lặp for, đặt b == falsethành !btức là b = true... Thread 2thực hiện công cụ của nó và rời khỏi vòng lặp for, đặt b == truethành !bnghĩa là b = falsecho phép toàn bộ quá trình được lặp lại vô thời hạn)
CharonX

3
Tôi thấy thật mỉa mai khi một trong những giải pháp tao nhã nhất cho một vấn đề, trong đó một số mã được cho là chỉ được thực thi một lần, là một vòng lặp . +1
nada

2
Tôi sẽ tránh b=!b, điều đó có vẻ tốt, nhưng bạn thực sự muốn giá trị là sai, vì vậy b=falsesẽ được ưu tiên.
yo '

2
Xin lưu ý rằng khối được bảo vệ sẽ chạy lại nếu nó không chạy cục bộ. Điều đó thậm chí có thể được mong muốn, nhưng nó khác với tất cả các phương pháp khác.
Davis Herring

29

Trong C ++ 17 bạn có thể viết

if (static int i; i == 0 && (i = 1)){

để tránh chơi xung quanh với itrong cơ thể vòng lặp. ibắt đầu bằng 0 (được đảm bảo bởi tiêu chuẩn) và biểu thức sau các ;tập hợp icho 1lần đầu tiên được đánh giá.

Lưu ý rằng trong C ++ 11, bạn có thể đạt được điều tương tự với chức năng lambda

if ([]{static int i; return i == 0 && (i = 1);}()){

cũng mang một lợi thế nhỏ trong đó ikhông bị rò rỉ vào thân vòng lặp.


4
Tôi rất buồn khi nói - nếu điều đó được đưa vào một #define được gọi là CALL_ONCE hoặc vì vậy nó sẽ dễ đọc hơn
nada

9
Mặc dù static int i;có thể (tôi thực sự không chắc chắn) là một trong những trường hợp iđược đảm bảo khởi tạo 0, nhưng sử dụng static int i = 0;ở đây rõ ràng hơn nhiều .
Kyle Willmon

7
Bất kể tôi đồng ý một trình khởi tạo là một ý tưởng tốt để hiểu
Các cuộc đua nhẹ nhàng trong quỹ đạo

5
@Bathsheba Kyle thì không, vì vậy khẳng định của bạn đã được chứng minh là sai. Bạn phải trả thêm bao nhiêu để thêm hai ký tự vì mục đích rõ ràng? Thôi nào, bạn là một "kiến trúc sư trưởng phần mềm"; bạn nên biết điều này :)
Các cuộc đua nhẹ nhàng trong quỹ đạo

5
Nếu bạn nghĩ việc đánh vần giá trị ban đầu cho một biến là trái ngược với rõ ràng hoặc gợi ý rằng "điều gì đó thú vị đang xảy ra", tôi không nghĩ bạn có thể được giúp đỡ;)
Các cuộc đua Lightness trong Orbit

14
static bool once = [] {
  std::cout << "Hello one-shot\n";
  return false;
}();

Giải pháp này là chủ đề an toàn (không giống như nhiều đề xuất khác).


3
Bạn có biết ()là tùy chọn (nếu trống) trên tờ khai lambda?
Nonyme

9

Bạn có thể bao bọc hành động một lần trong hàm tạo của một đối tượng tĩnh mà bạn khởi tạo thay cho điều kiện.

Thí dụ:

#include <iostream>
#include <functional>

struct do_once {
    do_once(std::function<void(void)> fun) {
        fun();
    }
};

int main()
{
    for (int i = 0; i < 3; ++i) {
        static do_once action([](){ std::cout << "once\n"; });
        std::cout << "Hello World\n";
    }
}

Hoặc bạn thực sự có thể gắn bó với một macro, có thể trông giống như thế này:

#include <iostream>

#define DO_ONCE(exp) \
do { \
  static bool used_before = false; \
  if (used_before) break; \
  used_before = true; \
  { exp; } \
} while(0)  

int main()
{
    for (int i = 0; i < 3; ++i) {
        DO_ONCE(std::cout << "once\n");
        std::cout << "Hello World\n";
    }
}

8

Giống như @damon đã nói, bạn có thể tránh sử dụng std::exchangebằng cách sử dụng số nguyên giảm dần, nhưng bạn phải nhớ rằng các giá trị âm giải quyết thành đúng. Cách sử dụng này sẽ là:

if (static int n_times = 3; n_times && n_times--)
{
    std::cout << "Hello world x3" << std::endl;
} 

Dịch cái này sang trình bao bọc ưa thích của @ Acorn sẽ như thế này:

struct n_times {
    int n;
    n_times(int number) {
        n = number;
    };
    explicit operator bool() {
        return n && n--;
    };
};

...

if(static n_times _(2); _)
{
    std::cout << "Hello world twice" << std::endl;
}

7

Mặc dù sử dụng std::exchangetheo đề xuất của @Acorn có lẽ là cách thành ngữ nhất, một hoạt động trao đổi không nhất thiết phải rẻ. Mặc dù tất nhiên việc khởi tạo tĩnh được đảm bảo an toàn cho luồng (trừ khi bạn nói với trình biên dịch của mình không làm điều đó), do đó, bất kỳ cân nhắc nào về hiệu suất dù sao cũng có phần vô ích khi có statictừ khóa.

Nếu bạn lo lắng về tối ưu hóa vi mô (như mọi người thường sử dụng C ++), bạn cũng có thể cào boolvà sử dụng intthay thế, điều này sẽ cho phép bạn sử dụng giảm dần (hay đúng hơn là tăng dần , vì không giống như boolgiảm dần intsẽ không bão hòa về 0 ...):

if(static int do_once = 0; !do_once++)

Trước đây, nó từng boolcó các toán tử tăng / giảm, nhưng chúng không được dùng nữa từ lâu (C ++ 11? Không chắc chắn?) Và sẽ bị xóa hoàn toàn trong C ++ 17. Tuy nhiên, bạn có thể giảm giá inttốt, và tất nhiên nó sẽ hoạt động như một điều kiện Boolean.

Phần thưởng: Bạn có thể thực hiện do_twicehoặc do_thricetương tự ...


Tôi đã thử nghiệm điều này và nó bắn nhiều lần trừ lần đầu tiên.
nada

@nada: Ngớ ngẩn với tôi, bạn đã đúng ... đã sửa nó. Nó được sử dụng để làm việc với boolvà giảm dần một lần. Nhưng gia tăng hoạt động tốt với int. Xem bản demo trực tuyến: coliru.stacked-crooking.com/a/ee83f677a9c0c85a
Damon

1
Điều này vẫn có vấn đề là nó có thể được thực thi rất nhiều lần, nó do_oncebao quanh và cuối cùng sẽ chạm 0 lần nữa (và một lần nữa và một lần nữa ...).
nada

Nói chính xác hơn: Điều này bây giờ sẽ được thực hiện mỗi lần INT_MAX.
nada

Vâng, nhưng ngoài bộ đếm vòng lặp cũng bao quanh trong trường hợp đó, nó hầu như không thành vấn đề. Rất ít người chạy 2 tỷ (hoặc 4 tỷ nếu không dấu) lặp đi lặp lại bất cứ thứ gì. Nếu có, họ vẫn có thể sử dụng số nguyên 64 bit. Sử dụng máy tính có sẵn nhanh nhất, bạn sẽ chết trước khi nó kết thúc, vì vậy bạn không thể bị kiện vì nó.
Damon

4

Dựa trên câu trả lời tuyệt vời của @ Bathsheba cho điều này - chỉ làm cho nó đơn giản hơn nữa.

Trong C++ 17, bạn có thể chỉ cần làm:

if (static int i; !i++) {
  cout << "Execute once";
}

(Trong các phiên bản trước, chỉ cần khai báo int ibên ngoài khối. Cũng hoạt động trong C :)).

Nói một cách đơn giản: bạn khai báo i, lấy giá trị mặc định là zero ( 0). Zero là falsey, do đó chúng tôi sử dụng !toán tử dấu chấm than ( ) để phủ định nó. Sau đó, chúng tôi sẽ tính đến thuộc tính gia tăng của <ID>++toán tử, đầu tiên được xử lý (được gán, v.v.) và sau đó tăng lên.

Do đó, trong khối này, tôi sẽ được khởi tạo và 0chỉ có giá trị một lần, khi khối được thực thi, và sau đó giá trị sẽ tăng. Chúng tôi chỉ đơn giản là sử dụng !toán tử để phủ nhận nó.


1
Nếu điều này đã được đăng sớm hơn, thì đây có lẽ là câu trả lời được chấp nhận nhất. Thật sự cảm ơn!
nada
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.