Niềng răng xoăn không cần thiết trong C ++?


186

Khi thực hiện đánh giá mã cho một đồng nghiệp ngày hôm nay tôi đã thấy một điều kỳ dị. Anh ta đã bao quanh mã mới của mình bằng các dấu ngoặc nhọn như thế này:

Constructor::Constructor()
{
   existing code

   {
      New code: do some new fancy stuff here
   }

   existing code
}

Kết quả là gì, nếu có, từ điều này? Điều gì có thể là lý do để làm điều này? Thói quen này đến từ đâu?

Biên tập:

Dựa trên đầu vào và một số câu hỏi dưới đây, tôi cảm thấy rằng tôi phải thêm một số câu hỏi, mặc dù tôi đã đánh dấu một câu trả lời.

Môi trường là các thiết bị nhúng. Có rất nhiều mã C kế thừa được bọc trong quần áo C ++. Có rất nhiều C đã phát triển C ++.

Không có phần quan trọng trong phần này của mã. Tôi chỉ thấy nó trong phần này của mã. Không có phân bổ bộ nhớ chính nào được thực hiện, chỉ có một số cờ được đặt và một số thay đổi đôi chút.

Mã được bao quanh bởi dấu ngoặc nhọn là:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

(Đừng bận tâm đến mã, chỉ cần bám vào các dấu ngoặc nhọn ...;)) Sau các dấu ngoặc nhọn, có thêm một chút vặn vẹo, kiểm tra trạng thái và báo hiệu cơ bản.

Tôi đã nói chuyện với anh chàng và động lực của anh ta là giới hạn phạm vi của các biến, đặt tên các cuộc đụng độ và một số khác mà tôi không thể thực sự chọn.

Từ POV của tôi, điều này có vẻ khá lạ và tôi không nghĩ rằng các dấu ngoặc nhọn nên có trong mã của chúng tôi. Tôi đã thấy một số ví dụ hay trong tất cả các câu trả lời về lý do tại sao người ta có thể bao quanh mã bằng dấu ngoặc nhọn, nhưng bạn không nên tách mã thành các phương thức thay thế?


98
Câu trả lời của đồng nghiệp của bạn là gì khi bạn hỏi anh ta tại sao anh ta làm điều đó?
Graham Borland

20
Khá phổ biến với mẫu RAII. Tổng quan nhanh: c2.com/cgi/wiki?ResourceAcquisitionIsInitialization
Marcin

9
Tôi ghét niềng răng không cần thiết
jacknad

8
Có bất kỳ tuyên bố trong khối bên trong?
Keith Thompson

15
có lẽ anh ta chỉ muốn dễ dàng 'gấp' phần đó trong trình soạn thảo của mình
wim

Câu trả lời:


280

Đôi khi thật tuyệt vì nó cung cấp cho bạn một phạm vi mới, nơi bạn có thể khai báo các biến mới (tự động) một cách "sạch sẽ" hơn.

Trong C++này có lẽ không quá quan trọng vì bạn có thể giới thiệu các biến mới bất cứ nơi nào, nhưng có lẽ là thói quen là từ C, nơi bạn không thể làm điều này cho đến khi C99. :)

C++có các hàm hủy, nên cũng có thể có các tài nguyên (tệp, mutexes, bất cứ thứ gì) được tự động phát hành khi thoát khỏi phạm vi, có thể làm cho mọi thứ sạch hơn. Điều này có nghĩa là bạn có thể giữ một số tài nguyên được chia sẻ trong thời gian ngắn hơn so với khi bạn nắm bắt nó khi bắt đầu phương thức.


37
+1 cho việc đề cập rõ ràng về các biến mới và thói quen cũ
arne

46
+1 để sử dụng phạm vi khối được sử dụng để giải phóng tài nguyên nhanh nhất có thể
Leo

9
Cũng dễ dàng 'nếu (0)' một khối.
vrdhn

Người đánh giá mã của tôi thường nói với tôi rằng tôi viết các hàm / phương thức quá ngắn. Để giữ cho họ hạnh phúc và để giữ cho mình hạnh phúc (nghĩa là tách biệt các mối quan tâm không liên quan, địa phương thay đổi, v.v.), tôi chỉ sử dụng kỹ thuật này như được xây dựng bởi @unwind.
ossandcad

21
@ossandcad, họ cho bạn biết phương pháp của bạn "quá ngắn"? Điều đó cực kỳ khó làm. 90% các nhà phát triển (có khả năng bao gồm cả tôi) có vấn đề ngược lại.
Ben Lee

169

Một mục đích có thể là kiểm soát phạm vi biến . Và vì các biến có lưu trữ tự động bị phá hủy khi chúng đi ra khỏi phạm vi, điều này cũng có thể cho phép một hàm hủy được gọi sớm hơn so với cách khác.


14
Tất nhiên, thực sự khối đó chỉ nên được tạo thành một chức năng riêng biệt.
BlueRaja - Daniel Pflughoeft

8
Lưu ý lịch sử: Đây là một kỹ thuật từ ngôn ngữ C ban đầu cho phép tạo các biến tạm thời cục bộ.
Thomas Matthews

12
Tôi phải nói rằng - mặc dù tôi hài lòng với câu trả lời của mình, nhưng đây thực sự không phải là câu trả lời hay nhất ở đây; các câu trả lời tốt hơn đề cập đến RAII một cách rõ ràng, vì đó là lý do chính tại sao bạn muốn một hàm hủy được gọi tại một điểm cụ thể. Điều này có vẻ giống như một trường hợp "súng nhanh nhất ở phương Tây": Tôi đã đăng đủ nhanh để tôi có đủ số lần nâng cấp sớm mà tôi đã đạt được "động lực" để đạt được mức tăng nhanh hơn một số câu trả lời tốt hơn. Không phải là tôi đang phàn nàn! :-)
ruakh

7
@ BlueRaja-DannyPflughoeft Bạn đang quá đơn giản. "Đặt nó trong một chức năng riêng biệt" không phải là giải pháp cho mọi vấn đề về mã. Mã trong một trong các khối này có thể được kết hợp chặt chẽ với mã xung quanh, chạm vào một số biến của nó. Sử dụng các hàm C, yêu cầu các thao tác con trỏ. Hơn nữa, không phải mọi đoạn mã đều (hoặc nên) có thể tái sử dụng và đôi khi mã có thể không có ý nghĩa gì. Đôi khi tôi đặt các khối xung quanh các fortuyên bố của mình để tạo ra một cuộc sống ngắn int i;trong C89. Chắc chắn là bạn không gợi ý rằng mỗi cái fornên ở một chức năng riêng biệt?
Anders Sjöqvist

101

Các dấu ngoặc nhọn được sử dụng để xác định phạm vi của biến được khai báo bên trong dấu ngoặc nhọn. Nó được thực hiện sao cho hàm hủy sẽ được gọi khi biến đi ra khỏi phạm vi. Trong hàm hủy, bạn có thể giải phóng một mutex (hoặc bất kỳ tài nguyên nào khác) để người khác có thể có được nó.

Trong mã sản xuất của tôi, tôi đã viết một cái gì đó như thế này:

void f()
{
   //some code - MULTIPLE threads can execute this code at the same time

   {
       scoped_lock lock(mutex); //critical section starts here

       //critical section code
       //EXACTLY ONE thread can execute this code at a time

   } //mutex is automatically released here

  //other code  - MULTIPLE threads can execute this code at the same time
}

Như bạn có thể thấy, theo cách này, bạn có thể sử dụng scoped_lock trong một hàm và đồng thời, có thể xác định phạm vi của nó bằng cách sử dụng thêm dấu ngoặc nhọn. Điều này đảm bảo rằng mặc dù mã bên ngoài dấu ngoặc nhọn có thể được thực thi bởi nhiều luồng đồng thời, mã bên trong dấu ngoặc sẽ được thực thi bởi chính xác một luồng tại một thời điểm.


1
Tôi nghĩ rằng nó sạch hơn chỉ cần có: scoped_lock lock (mutex) // mã phần quan trọng sau đó lock.unlock ().
bỏng

17
@szielenski: Nếu mã từ phần quan trọng ném ngoại lệ thì sao? Hoặc mutex sẽ bị khóa vĩnh viễn hoặc mã sẽ không sạch hơn như bạn đã nói.
Nawaz

4
@Nawaz: Cách tiếp cận của @ szielenski sẽ không khiến mutex bị khóa trong trường hợp ngoại lệ. Anh ta cũng sử dụng một scoped_lockthứ sẽ bị phá hủy trên các ngoại lệ. Tôi thường thích giới thiệu một phạm vi mới cho khóa, nhưng trong một số trường hợp, unlocknó rất hữu ích. Ví dụ: để khai báo một biến cục bộ mới trong phần quan trọng và sau đó sử dụng nó sau. (Tôi biết mình đến trễ, nhưng chỉ vì sự trọn vẹn ...)
Stephan

51

Như những người khác đã chỉ ra, một khối mới giới thiệu một phạm vi mới, cho phép một người viết một chút mã với các biến của chính nó không làm mất không gian tên của mã xung quanh và không sử dụng tài nguyên lâu hơn cần thiết.

Tuy nhiên, có một lý do tốt khác để làm điều này.

Nó chỉ đơn giản là để cô lập một khối mã đạt được mục đích (phụ) cụ thể. Thật hiếm khi một tuyên bố duy nhất đạt được hiệu ứng tính toán mà tôi muốn; thường phải mất vài Đặt những thứ đó trong một khối (có một bình luận) cho phép tôi nói với người đọc (thường là chính tôi vào một ngày sau đó):

  • Đoạn này có một mục đích khái niệm mạch lạc
  • Đây là tất cả các mã cần thiết
  • Và đây là một nhận xét về chunk.

ví dụ

{  // update the moving average
   i= (i+1) mod ARRAYSIZE;
   sum = sum - A[i];
   A[i] = new_value;
   sum = sum + new_value;
   average = sum / ARRAYSIZE ;  
}

Bạn có thể tranh luận tôi nên viết một hàm để làm tất cả điều đó. Nếu tôi chỉ làm điều đó một lần, viết một hàm chỉ cần thêm các cú pháp và tham số bổ sung; Có vẻ như ít điểm. Chỉ cần nghĩ về điều này như là một hàm không tham số, ẩn danh.

Nếu bạn may mắn, trình soạn thảo của bạn sẽ có chức năng gấp / mở, thậm chí sẽ cho phép bạn ẩn khối.

Tôi làm điều này tất cả các thời gian. Thật là một niềm vui lớn khi biết các giới hạn của mã tôi cần kiểm tra, và thậm chí tốt hơn để biết rằng nếu đoạn đó không phải là thứ tôi muốn, tôi không phải xem bất kỳ dòng nào.


23

Một lý do có thể là thời gian tồn tại của bất kỳ biến nào được khai báo bên trong khối dấu ngoặc nhọn mới được giới hạn trong khối này. Một lý do khác xuất hiện trong tâm trí là để có thể sử dụng mã gấp trong trình chỉnh sửa yêu thích.


17

Điều này giống như một khối if(hoặc whilevv ..), chỉ cần không có if . Nói cách khác, bạn giới thiệu một phạm vi mà không giới thiệu cấu trúc điều khiển.

"Phạm vi rõ ràng" này thường hữu ích trong các trường hợp sau:

  1. Để tránh đụng độ tên.
  2. Để phạm vi using.
  3. Để kiểm soát khi các hàm hủy được gọi.

Ví dụ 1:

{
    auto my_variable = ... ;
    // ...
}

// ...

{
    auto my_variable = ... ;
    // ...
}

Nếu my_variabletình cờ là một tên đặc biệt tốt cho hai biến khác nhau được sử dụng tách biệt với nhau, thì phạm vi rõ ràng cho phép bạn tránh phát minh ra một tên mới chỉ để tránh xung đột tên.

Điều này cũng cho phép bạn tránh sử dụng my_variablera khỏi phạm vi dự định của nó một cách tình cờ.

Ví dụ 2:

namespace N1 { class A { }; }
namespace N2 { class A { }; }

void foo() {

    {
        using namespace N1;
        A a; // N1::A.
        // ...
    }

    {
        using namespace N2;
        A a; // N2::A.
        // ...
    }

}

Các tình huống thực tế khi điều này hữu ích là rất hiếm và có thể cho thấy mã đã chín muồi để tái cấu trúc, nhưng cơ chế là ở đó nếu bạn thực sự cần nó.

Ví dụ 3:

{
    MyRaiiClass guard1 = ...;

    // ...

    {
        MyRaiiClass guard2 = ...;
        // ...
    } // ~MyRaiiClass for guard2 called.

    // ...

} // ~MyRaiiClass for guard1 called.

Điều này có thể quan trọng đối với RAII trong trường hợp khi nhu cầu giải phóng tài nguyên không tự nhiên "rơi" vào ranh giới của các chức năng hoặc cấu trúc điều khiển.


15

Điều này thực sự hữu ích khi sử dụng các khóa phạm vi kết hợp với các phần quan trọng trong lập trình đa luồng. Khóa phạm vi của bạn được khởi tạo trong dấu ngoặc nhọn (thường là lệnh đầu tiên) sẽ đi ra khỏi phạm vi ở cuối khối và vì vậy các luồng khác sẽ có thể chạy lại.


14

Mọi người khác đã bao quát chính xác phạm vi, RAII, v.v., nhưng vì bạn đề cập đến một môi trường nhúng, có một lý do tiềm năng nữa:

Có thể nhà phát triển không tin tưởng phân bổ đăng ký của nhà biên dịch này hoặc muốn kiểm soát rõ ràng kích thước khung ngăn xếp bằng cách giới hạn số lượng biến tự động trong phạm vi cùng một lúc.

Ở đây isInitcó khả năng sẽ nằm trên stack:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

Nếu bạn loại bỏ các dấu ngoặc nhọn, không gian isInitcó thể được dành riêng trong khung ngăn xếp ngay cả sau khi nó có thể được sử dụng lại: nếu có nhiều biến tự động có phạm vi cục bộ tương tự và kích thước ngăn xếp của bạn bị hạn chế, đó có thể là một vấn đề.

Tương tự, nếu biến của bạn được phân bổ cho một thanh ghi, việc đi ra khỏi phạm vi sẽ cung cấp một gợi ý mạnh mẽ rằng đăng ký hiện có sẵn để sử dụng lại. Bạn phải xem trình biên dịch được tạo có và không có dấu ngoặc để tìm hiểu xem điều này có tạo ra sự khác biệt thực sự không (và cấu hình nó - hoặc xem sự cố tràn ngăn xếp - để xem sự khác biệt này có thực sự quan trọng không).


Điểm tốt +1, mặc dù tôi khá chắc chắn các trình biên dịch hiện đại có quyền này mà không cần can thiệp. (IIRC - ít nhất là đối với các trình biên dịch không được nhúng - họ đã bỏ qua từ khóa 'đăng ký' từ tận '99 vì họ luôn có thể làm việc tốt hơn bạn có thể.)
Rup

11

Tôi nghĩ rằng những người khác đã bao phủ phạm vi, vì vậy tôi sẽ đề cập đến các niềng răng không cần thiết cũng có thể phục vụ mục đích trong quá trình phát triển. Ví dụ: giả sử bạn đang thực hiện tối ưu hóa cho một chức năng hiện có. Việc chỉnh sửa tối ưu hóa hoặc truy tìm lỗi đến một chuỗi các câu lệnh cụ thể rất đơn giản đối với người lập trình - xem bình luận trước khi niềng răng:

// if (false) or if (0) 
{
   //experimental optimization  
}

Cách thực hành này hữu ích trong các ngữ cảnh nhất định như gỡ lỗi, thiết bị nhúng hoặc mã cá nhân.


10

Tôi đồng ý với "ruakh". Nếu bạn muốn giải thích tốt về các cấp độ phạm vi khác nhau trong C, hãy xem bài đăng này:

Các cấp độ phạm vi khác nhau trong ứng dụng C

Nói chung, việc sử dụng "Phạm vi chặn" là hữu ích nếu bạn chỉ muốn sử dụng một biến tạm thời mà bạn không phải theo dõi trong suốt vòng đời của chức năng gọi. Ngoài ra, một số người sử dụng nó để bạn có thể sử dụng cùng một tên biến ở nhiều vị trí để thuận tiện, mặc dù đó thường không phải là một ý tưởng hay. ví dụ:

int unusedInt = 1;

int main(void) {
  int k;

  for(k = 0; k<10; k++) {
    int returnValue = myFunction(k);
    printf("returnValue (int) is: %d (k=%d)",returnValue,k);
  }

  for(k = 0; k<100; k++) {
    char returnValue = myCharacterFunction(k);
    printf("returnValue (char) is: %c  (k=%d)",returnValue,k);
  }

  return 0;
}

Trong ví dụ cụ thể này, tôi đã định nghĩa returnValue hai lần, nhưng vì nó chỉ ở phạm vi khối, thay vì phạm vi hàm (ví dụ: phạm vi hàm sẽ là, ví dụ, khai báo returnValue ngay sau int main (void)), tôi không nhận bất kỳ lỗi biên dịch nào, vì mỗi khối không biết đến trường hợp tạm thời của returnValue được khai báo.

Tôi không thể nói rằng đây là một ý tưởng hay nói chung (nghĩa là: có lẽ bạn không nên sử dụng lại các tên biến liên tục từ khối này sang khối khác), nhưng nói chung, nó tiết kiệm thời gian và cho phép bạn tránh phải quản lý giá trị của returnValue trên toàn bộ hàm.

Cuối cùng, xin lưu ý phạm vi của các biến được sử dụng trong mẫu mã của tôi:

int:  unusedInt:   File and global scope (if this were a static int, it would only be file scope)
int:  k:           Function scope
int:  returnValue: Block scope
char: returnValue: Block scope

Câu hỏi bận rộn, anh bạn. Tôi chưa bao giờ có 100 lần. Có gì đặc biệt về câu hỏi này? Liên kết tốt. C có giá trị hơn C ++.
Wolfpack'08

5

Vậy, tại sao nên sử dụng niềng răng "không cần thiết"?

  • Đối với mục đích "Phạm vi" (như đã đề cập ở trên)
  • Làm cho mã dễ đọc hơn theo cách (khá giống như sử dụng #pragmahoặc xác định "các phần" có thể được hiển thị)
  • Bởi vì bạn có thể. Đơn giản như vậy.

PS Đó không phải là mã BAD; nó hợp lệ 100%. Vì vậy, nó là một vấn đề của hương vị (không phổ biến).


5

Sau khi xem mã trong bản chỉnh sửa, tôi có thể nói rằng các dấu ngoặc không cần thiết có thể (trong chế độ xem của người viết mã gốc) để rõ ràng 100% những gì sẽ xảy ra trong if / then, ngay cả bây giờ chỉ là một dòng, nó có thể là một dòng nhiều dòng sau và dấu ngoặc đảm bảo bạn sẽ không gây ra lỗi.

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
   return -1;
}

nếu ở trên là bản gốc và loại bỏ "phần bổ sung" sẽ dẫn đến:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) 
     return isInit;
   return -1;
}

sau đó, một sửa đổi sau này có thể trông như thế này:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) 
     CallSomethingNewHere();
     return isInit;
   return -1;
}

và điều đó, tất nhiên, sẽ gây ra một vấn đề, vì bây giờ isInit sẽ luôn được trả về, bất kể nếu / sau đó.


4

Các đối tượng được tự động phá hủy khi chúng đi ra khỏi phạm vi ...


2

Một ví dụ khác về việc sử dụng là các lớp liên quan đến UI, đặc biệt là Qt.

Ví dụ: bạn có một số UI phức tạp và rất nhiều widget, mỗi trong số chúng có khoảng cách, bố cục riêng, v.v. Thay vì đặt tên cho chúng space1, space2, spaceBetween, layout1, ... bạn có thể tự cứu mình khỏi các tên không mô tả cho các biến chỉ tồn tại trong hai ba dòng mã.

Chà, một số người có thể nói rằng bạn nên chia nó thành các phương thức, nhưng việc tạo 40 phương thức không thể sử dụng lại trông không ổn - vì vậy tôi quyết định chỉ thêm dấu ngoặc nhọn và nhận xét trước chúng, vì vậy nó trông giống như khối logic. Thí dụ:

// Start video button 
{ 
   <here the code goes> 
}
// Stop video button
{
   <...>
}
// Status label
{
   <...>
}

Không thể nói đó là cách thực hành tốt nhất, nhưng nó tốt cho mã kế thừa.

Gặp phải những vấn đề này khi nhiều người đã thêm các thành phần của riêng họ vào UI và một số phương thức trở nên thực sự lớn, nhưng việc tạo ra 40 phương thức sử dụng một lần trong lớp đã bị rối tung là không thực tế.

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.