Làm thế nào để tạo một biến vòng lặp for const với ngoại lệ của câu lệnh tăng?


82

Hãy xem xét một tiêu chuẩn cho vòng lặp:

for (int i = 0; i < 10; ++i) 
{
   // do something with i
}

Tôi muốn ngăn không cho biến ibị sửa đổi trong phần thân của forvòng lặp.

Tuy nhiên, tôi không thể tuyên bố inhư constthế này làm cho tuyên bố không hợp lệ tăng. Có cách nào để tạo imột constbiến bên ngoài câu lệnh tăng không?


4
Tôi tin rằng không có cách nào để làm điều này
Itay

27
Điều này nghe có vẻ giống như một giải pháp để tìm kiếm một vấn đề.
Pete Becker

14
Biến phần nội dung vòng lặp for của bạn thành một hàm có const int iđối số. Khả năng thay đổi của chỉ mục chỉ được hiển thị khi cần thiết và bạn có thể sử dụng inlinetừ khóa để làm cho nó không ảnh hưởng đến đầu ra đã biên dịch.
Monty Thibault

4
Điều gì (hay đúng hơn là ai) có thể thay đổi giá trị của chỉ mục ngoài .... bạn? Bạn có mất lòng tin vào bản thân? Có thể là đồng nghiệp? Tôi đồng ý với @PeteBecker.
Z4-tier

4
@ Z4-tier Vâng, tất nhiên là tôi không tin tưởng vào bản thân. Tôi biết rằng tôi mắc sai lầm. Mọi lập trình viên giỏi đều biết. Đó là lý do tại sao chúng ta có những thứ như constbắt đầu.
Konrad Rudolph

Câu trả lời:


119

Từ c ++ 20, bạn có thể sử dụng các dải ô :: views :: iota như thế này:

for (int const i : std::views::iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Đây là một bản demo .


Từ c ++ 11, bạn cũng có thể sử dụng kỹ thuật sau, sử dụng IIILE (ngay lập tức được gọi ra biểu thức lambda nội tuyến):

int x = 0;
for (int i = 0; i < 10; ++i) [&,i] {
    std::cout << i << " ";  // ok, i is readable
    i = 42;                 // error, i is captured by non-mutable copy
    x++;                    // ok, x is captured by mutable reference
}();     // IIILE

Đây là một bản demo .

Lưu ý rằng điều đó [&,i]có nghĩa là nó iđược ghi lại bằng bản sao không thể thay đổi và mọi thứ khác được ghi lại bằng tham chiếu có thể thay đổi. Ở ();cuối vòng lặp chỉ đơn giản có nghĩa là lambda được gọi ngay lập tức.


Hầu hết các yêu cầu cho một cấu trúc vòng lặp for đặc biệt vì những gì điều này cung cấp là một sự thay thế an toàn hơn cho một cấu trúc rất, rất phổ biến.
Michael Dorgan

2
@MichaelDorgan Chà, giờ đã có thư viện hỗ trợ cho tính năng này, nên sẽ không đáng để thêm nó làm tính năng ngôn ngữ cốt lõi.
cigien

1
Công bằng, mặc dù hầu như tất cả công việc thực sự của tôi vẫn là C hoặc C ++ 11. Tôi nghiên cứu để phòng trường hợp nó quan trọng trong tương lai đối với tôi ...
Michael Dorgan

9
Thủ thuật C ++ 11 mà bạn đã thêm với lambda rất gọn gàng, nhưng sẽ không thực tế ở hầu hết các nơi làm việc mà tôi đã từng làm việc. Phân tích tĩnh sẽ phàn nàn về việc &nắm bắt tổng quát , điều này sẽ buộc phải nắm bắt từng tham chiếu một cách rõ ràng - điều này làm cho điều này khá cồng kềnh. Tôi cũng nghi ngờ rằng điều này có thể dẫn đến các lỗi dễ dàng mà tác giả quên (), làm cho mã không bao giờ được gọi. Điều này cũng dễ dàng bỏ sót trong quá trình xem xét mã.
Người biên dịch

1
@cigien Các công cụ phân tích tĩnh như SonarQube và cờ cppcheck nói chung nắm bắt như thế nào [&]vì những điều này xung đột với các tiêu chuẩn mã hóa như AUTOSAR (Quy tắc A5-1-2), HIC ++ và tôi nghĩ rằng cả MISRA (không chắc chắn). Nó không phải là nó không chính xác; đó là các tổ chức cấm loại mã này để tuân thủ các tiêu chuẩn. Đối với (), phiên bản gcc mới nhất không gắn cờ điều này ngay cả với -Wextra. Tôi vẫn nghĩ rằng cách tiếp cận là gọn gàng; nó chỉ không hoạt động cho nhiều tổ chức.
Human-Compiler

44

Đối với bất kỳ ai thích std::views::iotacâu trả lời của Cigien nhưng không hoạt động trong C ++ 20 trở lên, việc triển khai phiên bản std::views::iotatương thích đơn giản và nhẹ sẽ khá dễ dàng hoặc ở trên.

Tất cả những gì nó yêu cầu là:

  • Loại " LegacyInputIterator " cơ bản (thứ xác định operator++operator*) bao bọc một giá trị tích phân (ví dụ: an int)
  • Một số lớp giống như "phạm vi" có begin()end()trả về các trình vòng lặp ở trên. Điều này sẽ cho phép nó hoạt động trong các forvòng lặp dựa trên phạm vi

Một phiên bản đơn giản của điều này có thể là:

#include <iterator>

// This is just a class that wraps an 'int' in an iterator abstraction
// Comparisons compare the underlying value, and 'operator++' just
// increments the underlying int
class counting_iterator
{
public:
    // basic iterator boilerplate
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using reference  = int;
    using pointer    = int*;
    using difference_type = std::ptrdiff_t;

    // Constructor / assignment
    constexpr explicit counting_iterator(int x) : m_value{x}{}
    constexpr counting_iterator(const counting_iterator&) = default;
    constexpr counting_iterator& operator=(const counting_iterator&) = default;

    // "Dereference" (just returns the underlying value)
    constexpr reference operator*() const { return m_value; }
    constexpr pointer operator->() const { return &m_value; }

    // Advancing iterator (just increments the value)
    constexpr counting_iterator& operator++() {
        m_value++;
        return (*this);
    }
    constexpr counting_iterator operator++(int) {
        const auto copy = (*this);
        ++(*this);
        return copy;
    }

    // Comparison
    constexpr bool operator==(const counting_iterator& other) const noexcept {
        return m_value == other.m_value;
    }
    constexpr bool operator!=(const counting_iterator& other) const noexcept {
        return m_value != other.m_value;
    }
private:
    int m_value;
};

// Just a holder type that defines 'begin' and 'end' for
// range-based iteration. This holds the first and last element
// (start and end of the range)
// The begin iterator is made from the first value, and the
// end iterator is made from the second value.
struct iota_range
{
    int first;
    int last;
    constexpr counting_iterator begin() const { return counting_iterator{first}; }
    constexpr counting_iterator end() const { return counting_iterator{last}; }
};

// A simple helper function to return the range
// This function isn't strictly necessary, you could just construct
// the 'iota_range' directly
constexpr iota_range iota(int first, int last)
{
    return iota_range{first, last};
}

Tôi đã xác định ở trên với constexprnơi nó được hỗ trợ, nhưng đối với các phiên bản C ++ trước đó như C ++ 11/14, bạn có thể cần phải loại bỏ constexprnơi không hợp pháp trong các phiên bản đó để làm như vậy.

Bảng mẫu trên cho phép mã sau hoạt động trong phiên bản trước C ++ 20:

for (int const i : iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Điều này sẽ tạo ra cùng một assembly với std::views::iotagiải pháp C ++ 20 và forgiải pháp -loop cổ điển khi được tối ưu hóa.

Điều này hoạt động với bất kỳ trình biên dịch tuân thủ C ++ 11 nào (ví dụ như trình biên dịch như vậy gcc-4.9.4) và vẫn tạo ra một hợp ngữ gần giống với một forđối tác -loop cơ bản .

Lưu ý: Hàm iotatrợ giúp chỉ dành cho tính năng tương đương với std::views::iotagiải pháp C ++ 20 ; nhưng trên thực tế, bạn cũng có thể trực tiếp tạo một iota_range{...}thay vì gọi iota(...). Phần trước chỉ trình bày một đường dẫn nâng cấp dễ dàng nếu người dùng muốn chuyển sang C ++ 20 trong tương lai.


3
Nó yêu cầu một chút bản ghi sẵn, nhưng nó thực sự không phức tạp như vậy về những gì nó đang làm. Nó thực sự chỉ là một mẫu trình lặp cơ bản, nhưng bao bọc một int, sau đó tạo một lớp "phạm vi" để trả về bắt đầu / kết thúc
Human-Compiler

1
Không quá quan trọng, nhưng tôi cũng đã thêm một giải pháp c ++ 11 mà không ai khác đăng, vì vậy bạn có thể muốn nhập lại dòng đầu tiên của câu trả lời của mình một chút :)
cigien

Tôi không chắc ai đã phản đối, nhưng tôi đánh giá cao một số phản hồi nếu bạn cảm thấy câu trả lời của tôi không thỏa đáng để tôi có thể cải thiện nó. Từ chối là một cách tuyệt vời để cho thấy rằng bạn cảm thấy câu trả lời không giải quyết được đầy đủ câu hỏi, nhưng trong trường hợp này không có lời chỉ trích hoặc lỗi rõ ràng nào trong câu trả lời mà tôi có thể cải thiện.
Human-Compiler

@ Human-Compiler Tôi cũng nhận được một DV cùng lúc và họ cũng không bình luận về lý do tại sao :( Đoán ai đó không thích sự trừu tượng phạm vi. Tôi sẽ không lo lắng về điều đó.
cigien

1
"assembly" là một danh từ khối lượng như "hành lý" hoặc "nước". Cụm từ bình thường sẽ là "sẽ được biên dịch sang cùng một assembly với C ++ 20 ...". Đầu ra asm của trình biên dịch cho một chức năng không phải là một hợp ngữ đơn lẻ, mà là "hợp ngữ" (một chuỗi các lệnh hợp ngữ).
Peter Cordes

29

Phiên bản KISS ...

for (int _i = 0; _i < 10; ++_i) {
    const int i = _i;

    // use i here
}

Nếu trường hợp sử dụng của bạn chỉ là để ngăn chặn việc sửa đổi ngẫu nhiên chỉ mục vòng lặp thì điều này sẽ làm cho một lỗi như vậy rõ ràng. (Nếu bạn muốn ngăn chặn sự sửa đổi có chủ ý , thì, chúc may mắn ...)


11
Tôi nghĩ rằng bạn đã dạy sai bài học để sử dụng các dấu hiệu nhận biết ma thuật bắt đầu bằng _. Và một chút giải thích (ví dụ: phạm vi) sẽ hữu ích. Nếu không, vâng, tuyệt vời KISSy.
Yunnosch

14
Gọi biến "ẩn" i_sẽ tuân thủ hơn.
Yirkha

9
Tôi không chắc điều này trả lời câu hỏi như thế nào. Biến vòng lặp _ivẫn có thể sửa đổi được trong vòng lặp.
cigien

4
@cigien: IMO, giải pháp từng phần này đáng để sử dụng mà không có C ++ 20 std::views::iotađể có một cách hoàn toàn chống đạn. Nội dung câu trả lời giải thích những hạn chế của nó và cách nó cố gắng trả lời câu hỏi. Một loạt các C ++ 11 quá phức tạp làm cho việc chữa trị trở nên tồi tệ hơn căn bệnh về độ dễ đọc, dễ bảo trì, IMO. Điều này vẫn rất dễ đọc đối với tất cả những người biết C ++, và có vẻ hợp lý như một thành ngữ. (Nhưng nên tránh những tên gạch dưới đầu dòng.)
Peter Cordes

5
Chỉ @Yunnosch _Uppercasevà số double__underscorenhận dạng được bảo lưu. _lowercaseđịnh danh chỉ được bảo lưu trong phạm vi toàn cầu.
Roman Odaisky

13

Nếu bạn không có quyền truy cập vào , trang điểm điển hình bằng cách sử dụng một hàm

#include <vector>
#include <numeric> // std::iota

std::vector<int> makeRange(const int start, const int end) noexcept
{
   std::vector<int> vecRange(end - start);
   std::iota(vecRange.begin(), vecRange.end(), start);
   return vecRange;
}

bây giờ bạn có thể

for (const int i : makeRange(0, 10))
{
   std::cout << i << " ";  // ok
   //i = 100;              // error
}

( Xem Demo )


Cập nhật : Lấy cảm hứng từ bình luận của @ Human-Compiler , tôi đã tự hỏi thời tiết các câu trả lời đã cho có bất kỳ sự khác biệt nào trong trường hợp hiệu suất. Hóa ra, ngoại trừ cách tiếp cận này, tất cả các cách tiếp cận khác đều có cùng hiệu suất (đối với phạm vi [0, 10)) một cách đáng ngạc nhiên . Cách std::vectortiếp cận là tồi tệ nhất.

nhập mô tả hình ảnh ở đây

( Xem Ghế dài nhanh trực tuyến )


4
Mặc dù điều này hoạt động cho trước c ++ 20, nhưng điều này có một lượng chi phí khá lớn vì nó yêu cầu sử dụng vector. Nếu phạm vi rất lớn, điều này có thể không tốt.
Human-Compiler

@ Human-Compiler: A std::vectorkhá khủng khiếp trên quy mô tương đối nếu phạm vi cũng nhỏ, và có thể rất tệ nếu đây được cho là một vòng lặp nhỏ bên trong chạy nhiều lần. Một số trình biên dịch (như clang với libc ++, nhưng không phải libstdc ++) có thể tối ưu hóa xóa mới / xóa phân bổ không thoát khỏi hàm, nhưng nếu không thì điều này có thể dễ dàng là sự khác biệt giữa một vòng lặp nhỏ chưa được cuộn hoàn toàn so với lệnh gọi new+ deletevà có thể thực sự lưu trữ vào bộ nhớ đó.
Peter Cordes

IMO, lợi ích nhỏ của const inó đơn giản là không đáng giá đối với hầu hết các trường hợp, nếu không có C ++ 20 cách làm cho nó rẻ. Đặc biệt là với các phạm vi biến thời gian chạy khiến trình biên dịch ít có khả năng tối ưu hóa mọi thứ hơn.
Peter Cordes

13

Bạn không thể chỉ di chuyển một số hoặc tất cả nội dung của vòng lặp for của bạn trong một hàm chấp nhận tôi là một const?

Nó kém tối ưu hơn một số giải pháp được đề xuất, nhưng nếu có thể thì điều này khá đơn giản để thực hiện.

Chỉnh sửa: Chỉ là một ví dụ vì tôi có xu hướng không rõ ràng.

for (int i = 0; i < 10; ++i) 
{
   looper( i );
}

void looper ( const int v )
{
    // do your thing here
}

10

Và đây là phiên bản C ++ 11:

for (int const i : {0,1,2,3,4,5,6,7,8,9,10})
{
    std::cout << i << " ";
    // i = 42; // error
}

Đây là bản demo trực tiếp


6
Điều này không mở rộng nếu số tối đa được quyết định bởi một giá trị thời gian chạy.
Human-Compiler

12
@ Human-Compiler Đơn giản chỉ cần mở rộng danh sách đến giá trị mong muốn và biên dịch lại toàn bộ chương trình của bạn một cách động;)
Monty Thibault

5
Bạn đã không đề cập đến trường hợp của nó là gì {..}. Bạn cần bao gồm một số thứ để tính năng này hoạt động. Ví dụ: mã của bạn sẽ bị hỏng nếu bạn không thêm tiêu đề thích hợp: godbolt.org/z/esbhra . Tiếp tục <iostream>cho các tiêu đề khác là một ý tưởng tồi!
JeJo

6
#include <cstdio>
  
#define protect(var) \
  auto &var ## _ref = var; \
  const auto &var = var ## _ref

int main()
{
  for (int i = 0; i < 10; ++i) 
  {
    {
      protect(i);
      // do something with i
      //
      printf("%d\n", i);
      i = 42; // error!! remove this and it compiles.
    }
  }
}

Lưu ý: chúng ta cần lồng phạm vi vào vì có một sự ngu ngốc đáng kinh ngạc trong ngôn ngữ: biến được khai báo trong for(...)tiêu đề được coi là ở cùng mức lồng với các biến được khai báo trong {...}câu lệnh ghép. Điều này có nghĩa là, ví dụ:

for (int i = ...)
{
  int i = 42; // error: i redeclared in same scope
}

Gì? Không phải chúng ta vừa mở một dấu ngoặc nhọn sao? Hơn nữa, nó không nhất quán:

void fun(int i)
{
  int i = 42; // OK
}

1
Đây dễ dàng là câu trả lời tốt nhất. Tận dụng 'đổ bóng biến' của C ++ để khiến mã định danh phân giải thành biến const ref tham chiếu đến biến bước ban đầu, là một giải pháp hữu ích. Hoặc ít nhất, một trong những thanh lịch nhất có sẵn.
Max Barraclough

4

Một cách tiếp cận đơn giản chưa được đề cập ở đây hoạt động trong bất kỳ phiên bản nào của C ++ là tạo một trình bao bọc chức năng xung quanh một phạm vi, tương tự như những gì std::for_eachlàm với trình vòng lặp. Sau đó, người dùng có trách nhiệm chuyển đối số chức năng dưới dạng một lệnh gọi lại sẽ được gọi trên mỗi lần lặp.

Ví dụ:

// A struct that holds the start and end value of the range
struct numeric_range
{
    int start;
    int end;

    // A simple function that wraps the 'for loop' and calls the function back
    template <typename Fn>
    void for_each(const Fn& fn) const {
        for (auto i = start; i < end; ++i) {
            const auto& const_i = i;
            fn(const_i);
        }
    }
};

Việc sử dụng sẽ ở đâu:

numeric_range{0, 10}.for_each([](const auto& i){
   std::cout << i << " ";  // ok
   //i = 100;              // error
});

Bất kỳ thứ gì cũ hơn C ++ 11 sẽ bị mắc kẹt khi truyền một con trỏ hàm có tên mạnh vào for_each(tương tự như std::for_each), nhưng nó vẫn hoạt động.

Đây là bản demo


Mặc dù điều này có thể không phải là thành ngữ đối với forcác vòng lặp trong C ++ , nhưng cách tiếp cận này khá phổ biến trong các ngôn ngữ khác. Các trình bao bọc chức năng thực sự đẹp mắt vì khả năng kết hợp của chúng trong các câu lệnh phức tạp và có thể rất tiện lợi khi sử dụng.

Mã này cũng đơn giản để viết, hiểu và duy trì.


Một hạn chế cần lưu ý đối với cách tiếp cận này là một số tổ chức cấm chụp lambda mặc định (ví dụ [&]hoặc [=]) để tuân thủ các tiêu chuẩn an toàn nhất định, điều này có thể làm phồng lambda với mỗi thành viên cần được chụp thủ công. Không phải tất cả các tổ chức đều làm điều này, vì vậy tôi chỉ đề cập đến vấn đề này như một bình luận hơn là trong câu trả lời.
Người biên dịch

0
template<class T = int, class F>
void while_less(T n, F f, T start = 0){
    for(; start < n; ++start)
        f(start);
}

int main()
{
    int s = 0;
    
    while_less(10, [&](auto i){
        s += i;
    });
    
    assert(s == 45);
}

có thể gọi nó for_i

Không tốn phí https://godbolt.org/z/e7asGj

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.