C có tương đương với std :: less từ C ++ không?


26

Gần đây tôi đã trả lời một câu hỏi về hành vi không xác định khi thực hiện p < qtrong C khi pqlà các con trỏ vào các đối tượng / mảng khác nhau. Điều đó khiến tôi suy nghĩ: C ++ có hành vi (không xác định) tương tự <trong trường hợp này, nhưng cũng cung cấp mẫu thư viện chuẩn std::lessđược đảm bảo trả về điều tương tự như <khi con trỏ có thể được so sánh và trả về một số thứ tự nhất quán khi không thể.

C có cung cấp một cái gì đó có chức năng tương tự cho phép so sánh một cách an toàn các con trỏ tùy ý (với cùng loại) không? Tôi đã thử xem qua tiêu chuẩn C11 và không tìm thấy gì, nhưng trải nghiệm của tôi về C là các đơn đặt hàng có cường độ nhỏ hơn so với C ++, vì vậy tôi có thể dễ dàng bỏ lỡ điều gì đó.


1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

Câu trả lời:


20

Trên các triển khai với một mô hình bộ nhớ phẳng (về cơ bản là tất cả mọi thứ), việc chuyển sang uintptr_tsẽ chỉ hoạt động.

(Nhưng thấy so sánh con trỏ nên được ký kết hoặc unsigned trong 64-bit x86? Để thảo luận về việc liệu bạn nên đối xử với con trỏ như đã ký kết hay không, bao gồm cả các vấn đề hình thành con trỏ bên ngoài của đối tượng đó là UB trong C.)

Nhưng các hệ thống với các mô hình bộ nhớ không phẳng tồn tại và suy nghĩ về chúng có thể giúp giải thích tình hình hiện tại, như C ++ có các thông số kỹ thuật khác nhau <so với std::less.


Một phần của điểm <trên các con trỏ để phân tách các đối tượng là UB trong C (hoặc ít nhất là không xác định trong một số phiên bản C ++) là cho phép các máy lạ, bao gồm các mô hình bộ nhớ không phẳng.

Một ví dụ nổi tiếng là chế độ thực x86-16 trong đó con trỏ là phân đoạn: offset, tạo thành địa chỉ tuyến tính 20 bit thông qua (segment << 4) + offset. Cùng một địa chỉ tuyến tính có thể được biểu diễn bằng nhiều kết hợp seg: off khác nhau.

C ++ std::lesstrên các con trỏ trên các ISA kỳ lạ có thể cần phải đắt tiền , ví dụ: "bình thường hóa" một phân đoạn: bù vào x86-16 để có offset <= 15. Tuy nhiên, không có cách di động nào để thực hiện điều này. Thao tác cần thiết để chuẩn hóa một uintptr_t(hoặc biểu diễn đối tượng của đối tượng con trỏ) là cụ thể thực hiện.

Nhưng ngay cả trên các hệ thống mà C ++ std::lessphải đắt tiền, <cũng không phải như vậy. Ví dụ: giả sử mô hình bộ nhớ "lớn" trong đó một đối tượng nằm gọn trong một phân đoạn, <chỉ có thể so sánh phần bù và thậm chí không bận tâm với phần phân đoạn. (Các con trỏ bên trong cùng một đối tượng sẽ có cùng phân khúc và nếu không thì UB trong C. C ++ 17 đã thay đổi thành "không xác định", điều này vẫn có thể cho phép bỏ qua chuẩn hóa và chỉ so sánh các độ lệch.) Điều này giả sử tất cả các con trỏ với bất kỳ phần nào của một đối tượng luôn sử dụng cùng một seggiá trị, không bao giờ bình thường hóa. Đây là những gì bạn mong muốn ABI yêu cầu cho một mô hình bộ nhớ "lớn" trái ngược với mô hình bộ nhớ "khổng lồ". (Xem thảo luận trong ý kiến ).

(Ví dụ, một mô hình bộ nhớ có thể có kích thước đối tượng tối đa là 64kiB, nhưng tổng không gian địa chỉ tối đa lớn hơn nhiều có nhiều chỗ cho nhiều đối tượng có kích thước tối đa như vậy. ISO C cho phép triển khai có giới hạn về kích thước đối tượng thấp hơn giá trị tối đa (không dấu) size_tcó thể biểu thị , SIZE_MAX. Ví dụ, ngay cả trên các hệ thống mô hình bộ nhớ phẳng, GNU C giới hạn kích thước đối tượng tối đa để PTRDIFF_MAXtính toán kích thước có thể bỏ qua tràn tràn đã ký.) Xem câu trả lời này và thảo luận trong các nhận xét.

Nếu bạn muốn cho phép các đối tượng lớn hơn một phân đoạn, bạn cần một mô hình bộ nhớ "khổng lồ" phải lo lắng về việc tràn phần bù của con trỏ khi thực hiện p++để lặp qua một mảng hoặc khi thực hiện lập chỉ mục / số học con trỏ. Điều này dẫn đến mã chậm hơn ở mọi nơi, nhưng có lẽ sẽ có nghĩa là p < qsẽ xảy ra để hoạt động cho các con trỏ tới các đối tượng khác nhau, bởi vì việc triển khai nhắm mục tiêu mô hình bộ nhớ "khổng lồ" thường sẽ chọn để giữ tất cả các con trỏ được bình thường hóa mọi lúc. Xem những gì gần, xa và con trỏ lớn? - một số trình biên dịch C thực cho chế độ thực x86 đã có tùy chọn biên dịch cho mô hình "khổng lồ" trong đó tất cả các con trỏ được mặc định là "khổng lồ" trừ khi được khai báo khác.

Phân đoạn chế độ thực x86 không phải là mô hình bộ nhớ không phẳng duy nhất có thể , nó chỉ là một ví dụ cụ thể hữu ích để minh họa cách nó được xử lý bởi các triển khai C / C ++. Trong thực tế, việc triển khai mở rộng ISO C với khái niệm farso với nearcon trỏ, cho phép các lập trình viên lựa chọn khi họ có thể thoát khỏi chỉ với việc lưu trữ / chuyển xung quanh phần bù 16 bit, liên quan đến một số phân đoạn dữ liệu phổ biến.

Nhưng việc triển khai ISO C thuần túy sẽ phải chọn giữa một mô hình bộ nhớ nhỏ (mọi thứ trừ mã trong cùng 64kiB với con trỏ 16 bit) hoặc lớn hoặc lớn với tất cả các con trỏ là 32 bit. Một số vòng có thể tối ưu hóa bằng cách chỉ tăng phần bù, nhưng các đối tượng con trỏ không thể được tối ưu hóa để nhỏ hơn.


Nếu bạn biết thao tác ma thuật là gì đối với bất kỳ triển khai cụ thể nào, bạn có thể thực hiện nó trong C thuần túy . Vấn đề là các hệ thống khác nhau sử dụng địa chỉ khác nhau và các chi tiết không được tham số hóa bởi bất kỳ macro di động nào.

Hoặc có thể không: nó có thể liên quan đến việc tìm kiếm thứ gì đó từ bảng phân đoạn đặc biệt hoặc thứ gì đó, ví dụ như chế độ được bảo vệ x86 thay vì chế độ thực trong đó phần phân đoạn của địa chỉ là một chỉ mục, không phải là giá trị được dịch chuyển trái. Bạn có thể thiết lập các phân đoạn chồng lấp một phần trong chế độ được bảo vệ và các phần chọn địa chỉ của các địa chỉ thậm chí sẽ không được sắp xếp theo thứ tự như các địa chỉ cơ sở phân khúc tương ứng. Nhận địa chỉ tuyến tính từ một con trỏ seg: off trong chế độ được bảo vệ x86 có thể liên quan đến một cuộc gọi hệ thống, nếu GDT và / hoặc LDT không được ánh xạ vào các trang có thể đọc được trong quy trình của bạn.

(Tất nhiên các hệ điều hành chính cho x86 sử dụng mô hình bộ nhớ phẳng để cơ sở phân đoạn luôn là 0 (ngoại trừ lưu trữ cục bộ sử dụng fshoặc gsphân đoạn) và chỉ sử dụng phần "bù" 32 bit hoặc 64 bit làm con trỏ .)

Bạn có thể thêm mã theo cách thủ công cho các nền tảng cụ thể khác nhau, ví dụ như mặc định giả định hoặc #ifdefmột cái gì đó để phát hiện chế độ thực x86 và chia uintptr_tthành hai nửa 16 bit để seg -= off>>4; off &= 0xf;sau đó kết hợp các phần đó lại thành một số 32 bit.


Tại sao nó sẽ là UB nếu phân khúc không bằng nhau?
Acorn

@Acorn: Có nghĩa là nói cách khác; đã sửa. con trỏ vào cùng một đối tượng sẽ có cùng phân khúc, khác UB.
Peter Cordes

Nhưng tại sao bạn nghĩ rằng đó là UB trong mọi trường hợp? (đảo ngược logic hay không, thực ra tôi cũng không để ý)
Acorn

p < qLà UB trong C nếu chúng trỏ đến các đối tượng khác nhau, phải không? Tôi biết p - q
Peter Cordes

1
@Acorn: Dù sao, tôi không thấy một cơ chế tạo bí danh (khác biệt: tắt, cùng một địa chỉ tuyến tính) trong một chương trình không có UB. Vì vậy, nó không giống như trình biên dịch phải đi ra ngoài để tránh điều đó; mọi quyền truy cập vào một đối tượng đều sử dụng seggiá trị của đối tượng đó và phần bù đó> = phần bù trong phân đoạn nơi đối tượng đó bắt đầu. C làm cho nó UB thực hiện nhiều thứ giữa các con trỏ tới các đối tượng khác nhau, bao gồm cả những thứ như tmp = a-bvà sau đó b[tmp]để truy cập a[0]. Thảo luận về bí danh con trỏ phân đoạn này là một ví dụ tốt về lý do tại sao sự lựa chọn thiết kế đó có ý nghĩa.
Peter Cordes

17

Tôi đã từng cố gắng tìm cách khắc phục điều này và tôi đã tìm ra một giải pháp hoạt động cho các đối tượng chồng chéo và trong hầu hết các trường hợp khác, giả sử trình biên dịch thực hiện điều "thông thường".

Trước tiên bạn có thể thực hiện đề xuất trong Cách triển khai memmove trong tiêu chuẩn C mà không cần bản sao trung gian? và sau đó nếu điều đó không hoạt động uintptr(một loại trình bao bọc cho một trong hai uintptr_thoặc unsigned long longtùy thuộc vào việc uintptr_tcó sẵn không) và nhận được kết quả chính xác rất có thể (mặc dù điều đó có thể không quan trọng bằng mọi cách):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}

5

Có phải C cung cấp một cái gì đó có chức năng tương tự sẽ cho phép so sánh một cách an toàn con trỏ tùy ý.

Không


Đầu tiên chúng ta chỉ xem xét con trỏ đối tượng . Con trỏ chức năng mang lại một loạt các mối quan tâm khác.

2 con trỏ p1, p2có thể có các bảng mã khác nhau và trỏ đến cùng một địa chỉ vì vậy p1 == p2mặc dù memcmp(&p1, &p2, sizeof p1)không phải là 0. Kiến trúc như vậy rất hiếm.

Tuy nhiên, việc chuyển đổi các con trỏ uintptr_tnày không yêu cầu cùng một kết quả số nguyên dẫn đến (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 bản thân nó là mã hợp pháp, bởi có thể không cung cấp hy vọng cho chức năng.


Nếu mã thực sự cần so sánh các con trỏ không liên quan, hãy tạo một hàm trợ giúp less(const void *p1, const void *p2)và thực hiện mã cụ thể của nền tảng ở đó.

Có lẽ:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}
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.