2013-03-05 27 views
24

Mã sau (thực hiện số học con trỏ trên các ranh giới con) có hành vi được xác định rõ ràng cho các loại T mà nó biên dịch (trong đó, trong C++ 11, does not not necessarily have to be POD) hoặc bất kỳ tập con nào không?Số học con trỏ trên các ranh giới subobject

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    // ensure alignment 
    union 
    { 
     T initial; 
     char begin; 
    }; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
    char end; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10); 
    assert(&d.end - &d.begin == sizeof(float) * 10); 
    return 0; 
} 

LLVM sử dụng một biến thể của kỹ thuật nói trên trong việc thực hiện một loại vector nội bộ được tối ưu hóa để sử dụng ban đầu ngăn xếp cho mảng nhỏ nhưng chuyển sang một bộ đệm đống phân bổ một lần trên công suất ban đầu. (Lý do để làm nó theo cách này là không rõ ràng từ ví dụ này, nhưng cũng có khả năng làm giảm mẫu mã sưng lên, điều này là rõ ràng hơn nếu bạn xem xét thông qua các code.)

LƯU Ý: Trước khi bất cứ ai phàn nàn, điều này là không chính xác những gì họ đang làm và có thể là cách tiếp cận của họ phù hợp hơn so với những gì tôi đã đưa ra ở đây, nhưng tôi muốn hỏi về trường hợp chung.

Rõ ràng, nó hoạt động trong thực tế, nhưng tôi tò mò nếu bất cứ điều gì trong tiêu chuẩn đảm bảo cho rằng đó là trường hợp. Tôi có khuynh hướng nói không, cho N3242/expr.add:

Khi hai con trỏ đến các yếu tố của đối tượng cùng một mảng được trừ, kết quả là sự khác biệt của các chỉ số của hai phần tử mảng. ..Hơn nữa, nếu biểu thức P trỏ đến một phần tử của đối tượng mảng hoặc một phần tử cuối cùng của đối tượng mảng , và biểu thức Q trỏ tới phần tử cuối cùng của cùng một đối tượng mảng, biểu thức ((Q) +1) - (P) có cùng giá trị với ((Q) - (P)) + 1 và as - ((P) - ((Q) +1)) và có giá trị bằng 0 nếu biểu thức P điểm một phần tử cuối cùng của đối tượng mảng, mặc dù biểu thức (Q) +1 không trỏ đến một phần tử của đối tượng mảng. ... Trừ khi cả hai con trỏ trỏ đến các phần tử của cùng một đối tượng mảng hoặc một phần tử cuối cùng của đối tượng mảng, hành vi đó là không xác định.

Nhưng về mặt lý thuyết, phần giữa của đoạn trích trên, kết hợp với cách bố trí lớp học và liên kết bảo lãnh, có thể cho phép (nhỏ) điều chỉnh sau đây là hợp lệ:

#include <cassert> 
#include <cstddef> 

template<typename T> 
struct Base 
{ 
    T initial[1]; 
}; 

template<typename T, size_t N> 
struct Derived : public Base<T> 
{ 
    T rest[N - 1]; 
}; 

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]); 
    assert(&d.rest[0] - &d.initial[0] == 1); 
    return 0; 
} 

mà kết hợp với các quy định khác khác nhau liên quan đến bố cục union, khả năng chuyển đổi đến và đi từ char *, v.v., có thể cho là mã ban đầu hợp lệ. (Vấn đề chính là thiếu độ nhạy trong định nghĩa số học con trỏ được nêu ở trên.)

Có ai biết chắc không? N3242/expr.add dường như làm rõ rằng con trỏ phải thuộc cùng một "đối tượng mảng" cho nó được xác định, nhưng có thể giả thiết là trường hợp đảm bảo khác trong tiêu chuẩn, khi được kết hợp với nhau, có thể yêu cầu một định nghĩa dù sao trong trường hợp này để duy trì một cách hợp lý tự nhất quán. (Tôi không đặt cược vào nó, nhưng tôi sẽ ít nhất là có thể tưởng tượng được.)

EDIT: @MatthieuM làm tăng phản đối rằng lớp này không phải là bố trí chuẩn và do đó có thể không được đảm bảo không chứa đệm giữa subobject cơ sở và thành viên đầu tiên của nguồn gốc, ngay cả khi cả hai đều được căn chỉnh với alignof(T).Tôi không chắc sự thật là thế nào, nhưng điều đó mở ra các câu hỏi biến thể sau:

  • Điều này có được đảm bảo để làm việc nếu di sản thừa kế đã bị xóa không?

  • Liệu &d.end - &d.begin >= sizeof(float) * 10 có được đảm bảo ngay cả khi &d.end - &d.begin == sizeof(float) * 10 không?

LAST EDIT @ArneMertz lập luận cho một đọc rất chặt chẽ của N3242/expr.add (vâng, tôi biết tôi đang đọc một bản thảo, nhưng nó đủ gần), nhưng không tiêu chuẩn thực sự ngụ ý rằng sau đây có hành vi không xác định sau đó nếu đường trao đổi được loại bỏ? (Định nghĩa cùng một lớp như trên)

int main() 
{ 
    Derived<float, 10> d; 
    bool aligned; 
    float * p = &d.initial[0], * q = &d.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
    } 

    assert(!aligned || d.rest[1] == 1.0); 

    return 0; 
} 

Ngoài ra, nếu == là không đủ mạnh, những gì nếu chúng ta tận dụng thực tế là std::less hình thức tổng trật tự trên con trỏ, và thay đổi các điều kiện trên để:

if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p)))) 

Mã có giả định rằng hai con trỏ bằng nhau trỏ đến cùng một đối tượng mảng thực sự bị hỏng theo một tiêu chuẩn đọc nghiêm ngặt không?

EDIT Xin lỗi, chỉ muốn thêm một ví dụ nữa, để loại bỏ các vấn đề bố trí tiêu chuẩn:

#include <cassert> 
#include <cstddef> 
#include <utility> 
#include <functional> 

// standard layout 
struct Base 
{ 
    float initial[1]; 
    float rest[9]; 
}; 

int main() 
{ 
    Base b; 
    bool aligned; 
    float * p = &b.initial[0], * q = &b.rest[0]; 

    ++p; 
    if((aligned = (p == q))) 
    { 
     std::swap(p, q); // does it matter if this line is removed? 
     *++p = 1.0; 
     q = &b.rest[1]; 
     // std::swap(p, q); // does it matter if this line is added? 
     p -= 2; // is this UB? 
    } 
    assert(!aligned || b.rest[1] == 1.0); 
    assert(p == &b.initial[0]); 

    return 0; 
} 
+6

Tôi không thể tin rằng có những câu hỏi hay trong thẻ C++. +1. –

+0

Có thể là một bản sao của [Liên kết phần tử công đoàn] (http://stackoverflow.com/questions/891471/union-element-alignment), nhưng tôi không chắc chắn –

+0

@ BЈовић câu hỏi này giả định sự hiểu biết về câu trả lời cho câu hỏi đó, thực sự –

Trả lời

8

Cập nhật: Câu trả lời này lúc đầu bỏ lỡ một số thông tin và do đó dẫn đến những kết luận sai.

Trong ví dụ của bạn, initialrest rõ ràng là khác biệt (mảng) đối tượng, vì vậy so sánh con trỏ để initial (hoặc các yếu tố của nó) với con trỏ để rest (hoặc các yếu tố của nó) được

  • UB, nếu bạn sử dụng sự khác biệt của các con trỏ. (§5.7,6)
  • không xác định, nếu bạn sử dụng toán tử quan hệ (§5.9,2)
  • được xác định rõ cho == (Vì vậy, các Snipped thứ hai là tốt, xem dưới đây)

đoạn đầu tiên:

Xây dựng sự khác biệt trong đoạn đầu tiên là undefined hành vi, cho quote mà bạn cung cấp (§5.7,6):

Trừ khi cả hai con trỏ poin t đến các phần tử của cùng một đối tượng mảng, hoặc một phần tử cuối cùng của đối tượng mảng, hành vi này là không xác định.

Để làm rõ những phần UB của mã ví dụ đầu tiên:

//first example 
int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.initial == 10);   //!!! UB !!! 
    assert(&d.end - &d.begin == sizeof(float) * 10); //!!! UB !!! (*) 
    return 0; 
} 

Dòng được đánh dấu bằng (*) là thú vị: d.begind.end không yếu tố của mảng tương tự và do đó kết quả hoạt động trong UB.Điều này mặc dù thực tế bạn có thể reinterpret_cast<char*>(&d) và có cả hai địa chỉ của chúng trong mảng kết quả. Nhưng vì mảng đó đại diện cho tất cả của d, không được xem là quyền truy cập vào các phần trong số d. Vì vậy, trong khi đó hoạt động có lẽ sẽ chỉ làm việc và đưa ra kết quả mong đợi trên bất kỳ ai thực hiện có thể mơ ước, nó vẫn là UB - như một vấn đề của định nghĩa.

đoạn thứ hai:

này là thực sự được xác định rõ hành vi, nhưng thực hiện được xác định kết quả:

int main() 
{ 
    Derived<float, 10> d; 
    assert(&d.rest[9] - &d.rest[0] == 9); 
    assert(&d.rest[0] == &d.initial[1]);   //(!) 
    assert(&d.initial[1] - &d.initial[0] == 1); 
    return 0; 
} 

Dòng được đánh dấu bằng (!)không ub, nhưng kết quả của nó là thực hiện quy định, kể từ khi padding, alignment và instumentation được đề cập có thể đóng một vai trò. Nhưng nếu xác nhận đó sẽ giữ, bạn có thể sử dụng hai phần đối tượng như một mảng.

Bạn sẽ biết rằng rest[0] sẽ đặt ngay sau initial[0] trong bộ nhớ. Thoạt nhìn, bạn có thể không dễ dàng sử dụng bình đẳng:

  • initial[1] sẽ chỉ một quá khứ-the-end của initial, dereferencing nó là UB.
  • rest[-1] rõ ràng nằm ngoài giới hạn.

Nhưng vào §3.9.2,3:

Nếu một đối tượng kiểu T tọa lạc tại một địa chỉ A, một con trỏ kiểu cvT* có giá trị là địa chỉ A được cho là trỏ đến đối tượng đó, bất kể giá trị đã thu được như thế nào. [Lưu ý: Ví dụ: địa chỉ một quá khứ của một mảng (5.7) sẽ được xem là trỏ đến một đối tượng không liên quan thuộc loại phần tử của loại có thể được đặt tại địa chỉ đó.

Vì vậy, miễn là &initial[1] == &rest[0], nó sẽ là nhị phân giống như nếu chỉ có một mảng và tất cả sẽ ổn.

Bạn có thể lặp qua cả hai mảng vì bạn có thể áp dụng một số "chuyển ngữ cảnh con trỏ" ở ranh giới. Vì vậy, để đoạn cuối cùng của bạn: swap là không cần thiết!

Tuy nhiên, có một số báo trước: rest[-1] là UB, và như vậy sẽ được initial[2], vì §5.7,5:

Nếu cả hai toán hạng trỏ và điểm kết quả đến các yếu tố của cùng một mảng đối tượng, hoặc một trong quá khứ phần tử cuối cùng của đối tượng mảng, việc đánh giá sẽ không tạo ra tràn; nếu không, hành vi là không xác định.

(nhấn mạnh mỏ). Vậy làm thế nào để hai phù hợp với nhau?

  • "Good đường": &initial[1] là ok, và kể từ &initial[1] == &rest[0] bạn có thể lấy địa chỉ đó và tiếp tục tăng con trỏ để truy cập các yếu tố khác của rest, vì §3.9.2,3
  • " Đường dẫn xấu ": initial[2]*(initial + 2), nhưng vì §5,7,5, initial +2 đã là UB và bạn không bao giờ được sử dụng §3.9.2,3 tại đây.

Cùng nhau: bạn phải dừng lại ở ranh giới, hãy nghỉ ngắn để kiểm tra xem địa chỉ có bằng nhau không và sau đó bạn có thể tiếp tục.

+0

Tôi tin bạn, nhưng điều đó có nghĩa là nó không thể thực hiện được ' std :: memset' hoặc bất cứ điều gì tương tự như mình sau đó mà không cần gọi UB sau đó, bất kể có hay không một đối tượng được bố trí tiêu chuẩn hay không? Bạn sẽ phải sử dụng các chức năng được cung cấp dưới dạng nguyên thủy hoặc khác? –

+0

(Nhân tiện, chỉnh sửa của tôi bị từ chối, nhưng tôi nghĩ bạn có nghĩa là 'float *' không 'int *') –

+0

Tôi sẽ chấp nhận điều này nếu bạn làm rõ nếu bạn nghĩ ví dụ cuối cùng trong câu hỏi đã chỉnh sửa của tôi là UB hay không. (Nó có thể là, về mặt kỹ thuật, chỉ tò mò những gì bạn nghĩ mặc dù.) –

Các vấn đề liên quan