Sự thay đổi về chủ đề tuyệt vời kiểu: xây dựng tầm thường tại chỗ


9

Tôi biết đây là một chủ đề khá phổ biến, nhưng nhiều như UB thông thường rất dễ tìm, cho đến nay tôi không tìm thấy biến thể này.

Vì vậy, tôi đang cố gắng chính thức giới thiệu các đối tượng Pixel trong khi tránh một bản sao thực sự của dữ liệu.

Điều này có hợp lệ không?

struct Pixel {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t alpha;
};

static_assert(std::is_trivial_v<Pixel>);

Pixel* promote(std::byte* data, std::size_t count)
{
    Pixel * const result = reinterpret_cast<Pixel*>(data);
    while (count-- > 0) {
        new (data) Pixel{
            std::to_integer<uint8_t>(data[0]),
            std::to_integer<uint8_t>(data[1]),
            std::to_integer<uint8_t>(data[2]),
            std::to_integer<uint8_t>(data[3])
        };
        data += sizeof(Pixel);
    }
    return result; // throw in a std::launder? I believe it is not mandatory here.
}

Mô hình sử dụng dự kiến, rất đơn giản:

std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data

Cụ thể hơn:

  • Liệu mã này có hành vi được xác định rõ?
  • Nếu có, nó có an toàn khi sử dụng con trỏ trả về không?
  • Nếu có, những Pixelloại khác nó có thể được mở rộng? (nới lỏng hạn chế is_trivial? pixel chỉ với 3 thành phần?).

Cả clang và gcc đều tối ưu hóa toàn bộ vòng lặp thành hư vô, đó là điều tôi muốn. Bây giờ, tôi muốn biết liệu điều này có vi phạm một số quy tắc C ++ hay không.

Godbolt liên kết nếu bạn muốn chơi xung quanh nó.

(lưu ý: Tôi không gắn thẻ c ++ 17 mặc dù std::bytevì câu hỏi vẫn sử dụng char)


2
Nhưng Pixels tiếp giáp được đặt mới vẫn không phải là một mảng của Pixels.
Jarod42

1
@spectras Điều đó không tạo ra một mảng mặc dù. Bạn chỉ cần có một loạt các đối tượng Pixel cạnh nhau. Điều đó khác với một mảng.
NathanOliver

1
Vì vậy, không có nơi nào bạn làm pixels[some_index]hoặc *(pixels + something)? Đó sẽ là UB.
NathanOliver

1
Phần có liên quan ở đây và cụm từ chính là nếu P trỏ đến một phần tử mảng i của một đối tượng mảng x . Ở đây pixels(P) không phải là một con trỏ tới đối tượng mảng mà là một con trỏ tới một đối tượng Pixel. Điều đó có nghĩa là bạn chỉ có thể truy cập pixels[0]hợp pháp.
NathanOliver

3
Bạn muốn đọc wg21.link/P0593 .
ecatmur

Câu trả lời:


3

Đó là hành vi không xác định để sử dụng kết quả của promotemột mảng. Nếu chúng ta nhìn vào [expr.add] /4.2, chúng ta có

Mặt khác, nếu Ptrỏ đến một phần tử mảng icủa một đối tượng mảngxncác phần tử ([dcl.array]), các biểu thức P + JJ + P(trong đó Jcó giá trị j) trỏ đến phần tử mảng (có thể giả định) i+jcủa xif 0≤i+j≤nvà biểu thức P - Jtrỏ đến ( yếu tố mảng có thể giả định) i−jcủa xif 0≤i−j≤n.

chúng ta thấy rằng nó yêu cầu con trỏ thực sự trỏ đến một đối tượng mảng. Bạn không thực sự có một đối tượng mảng mặc dù. Bạn có một con trỏ tới một điểm Pixelmà chỉ có một con trỏ Pixelstheo sau nó trong bộ nhớ liền kề. Điều đó có nghĩa là yếu tố duy nhất bạn thực sự có thể truy cập là yếu tố đầu tiên. Cố gắng truy cập bất cứ điều gì khác sẽ là hành vi không xác định bởi vì bạn đã qua cuối tên miền hợp lệ cho con trỏ.


Cảm ơn vì đã tìm ra nhanh chóng. Tôi sẽ làm một vòng lặp thay vì tôi đoán. Là một sidenote, điều này cũng có nghĩa &somevector[0] + 1là UB (ý tôi là, sử dụng con trỏ kết quả sẽ là).
quang phổ

@spectras Điều đó thực sự ổn. Bạn luôn có thể đưa con trỏ đến một đối tượng. Bạn không thể bỏ qua con trỏ đó, ngay cả khi có một đối tượng hợp lệ ở đó.
NathanOliver

Vâng, tôi đã chỉnh sửa nhận xét để làm cho mình rõ ràng hơn, ý tôi là hủy bỏ con trỏ kết quả :) Cảm ơn bạn đã xác nhận.
quang phổ

@spectras Không có vấn đề. Phần này của C ++ có thể rất khó khăn. Mặc dù phần cứng sẽ làm những gì chúng ta muốn nó làm, nhưng đó không thực sự là mã hóa. Chúng tôi đang mã hóa cho máy trừu tượng C ++ và đó là một máy mạnh mẽ;) Hy vọng P0593 sẽ được chấp nhận và điều này sẽ trở nên dễ dàng hơn nhiều.
NathanOliver

1
@spectras Không, bởi vì một vectơ std được định nghĩa là có chứa một mảng và bạn có thể thực hiện số học con trỏ giữa các phần tử mảng. Đáng buồn thay, không có cách nào để thực hiện vector std trong chính C ++, đáng buồn thay, không chạy vào UB.
Yakk - Adam Nevraumont

1

Bạn đã có câu trả lời liên quan đến việc sử dụng hạn chế của con trỏ được trả về, nhưng tôi muốn thêm rằng tôi cũng nghĩ bạn cần std::laundercó thể truy cập đầu tiên Pixel:

Việc reinterpret_castnày được thực hiện trước khi bất kỳ Pixelđối tượng nào được tạo (giả sử bạn không làm như vậy getSomeImageData). Do đó reinterpret_castsẽ không thay đổi giá trị con trỏ. Con trỏ kết quả vẫn sẽ trỏ đến phần tử đầu tiên của std::bytemảng được truyền cho hàm.

Khi bạn tạo các Pixelđối tượng, chúng sẽ được lồng trong std::bytemảng và std::bytemảng sẽ cung cấp lưu trữ cho các Pixelđối tượng.

Có những trường hợp sử dụng lại lưu trữ làm cho một con trỏ đến đối tượng cũ tự động trỏ đến đối tượng mới. Nhưng đây không phải là những gì đang xảy ra ở đây, vì vậy resultvẫn sẽ chỉ vào std::byteđối tượng, không phải Pixelđối tượng. Tôi đoán sử dụng nó như thể nó đang chỉ vào một Pixelđối tượng về mặt kỹ thuật sẽ là hành vi không xác định.

Tôi nghĩ rằng điều này vẫn giữ, ngay cả khi bạn thực hiện reinterpret_castsau khi tạo Pixelđối tượng, vì Pixelđối tượng và đối tượng std::bytecung cấp lưu trữ cho nó không thể chuyển đổi được con trỏ . Vì vậy, ngay cả sau đó con trỏ sẽ tiếp tục trỏ đến std::byte, không phải Pixelđối tượng.

Nếu bạn đã lấy được con trỏ để trả về từ kết quả của một trong những vị trí mới, thì mọi thứ sẽ ổn, cho đến khi Pixelcó liên quan đến quyền truy cập vào đối tượng cụ thể đó.


Ngoài ra, bạn cần đảm bảo rằng std::bytecon trỏ được căn chỉnh phù hợp Pixelvà mảng thực sự đủ lớn. Theo như tôi nhớ thì tiêu chuẩn không thực sự đòi hỏi phải Pixelcó sự liên kết giống như std::bytehoặc nó không có phần đệm.


Ngoài ra không ai trong số này phụ thuộc vào Pixeltầm thường hoặc thực sự bất kỳ tài sản nào khác của nó. Mọi thứ sẽ hoạt động theo cùng một cách miễn là std::bytemảng có đủ kích thước và được căn chỉnh phù hợp cho các Pixelđối tượng.


Tôi tin đó là chính xác. Thậm chí nếu điều mảng (unimplementability của std::vector) không phải là một vấn đề, bạn vẫn sẽ cần phải std::launderkết quả trước khi truy cập bất kỳ theo vị trí newed Pixels. Ngay bây giờ, std::launderđây là UB, vì các Pixels liền kề sẽ có thể truy cập được từ con trỏ giặt .
Fureeish

@Fureeish Tôi không chắc tại sao std::laundersẽ là UB nếu được áp dụng resulttrước khi quay lại. Liền kề Pixelkhông " có thể truy cập " thông qua con trỏ được giặt là theo hiểu biết của tôi về eel.is/c++draft/ptr.launder#4 . Và thậm chí tôi không thấy nó là UB như thế nào, bởi vì toàn bộ std::bytemảng ban đầu có thể truy cập được từ con trỏ ban đầu.
quả óc chó

Nhưng cái tiếp theo Pixelsẽ không thể truy cập được từ std::bytecon trỏ, mà là từ laundercon trỏ ed. Tôi tin rằng điều này có liên quan ở đây. Tôi rất vui khi được sửa chữa, mặc dù.
Fureeish

@Fureeish Từ những gì tôi có thể nói không có ví dụ nào được áp dụng ở đây và định nghĩa của yêu cầu cũng nói giống như tiêu chuẩn. Khả năng tiếp cận được xác định theo byte lưu trữ, không phải đối tượng. Các byte bị chiếm bởi Pixeldường như có thể tiếp cận được với tôi từ con trỏ ban đầu, bởi vì con trỏ ban đầu trỏ đến một phần tử của std::bytemảng chứa các byte tạo thành bộ lưu trữ để Pixeltạo " hoặc trong mảng bao quanh ngay lập tức mà Z là một yếu tố "điều kiện áp dụng (trong đó ZY, tức là std::byteyếu tố chính nó).
quả óc chó

Tôi nghĩ rằng các byte lưu trữ mà các Pixelchiếm đóng tiếp theo không thể truy cập được thông qua con trỏ được giặt, bởi vì Pixelđối tượng trỏ không phải là thành phần của một đối tượng mảng và cũng không thể chuyển đổi con trỏ với bất kỳ đối tượng có liên quan nào khác. Nhưng tôi cũng đang nghĩ về chi tiết này std::launderlần đầu tiên ở độ sâu đó. Tôi cũng không chắc chắn 100% về điều này.
quả óc chó
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.