Apakah C memiliki setara dengan std :: kurang dari C ++?

26

Saya baru-baru ini menjawab pertanyaan tentang perilaku yang tidak ditentukan dalam melakukan p < qC kapan pdan menunjuk qke objek / array yang berbeda. Itu membuat saya berpikir: C ++ memiliki perilaku yang sama (tidak terdefinisi) <dalam kasus ini, tetapi juga menawarkan templat pustaka standar std::lessyang dijamin untuk mengembalikan hal yang sama seperti <ketika pointer dapat dibandingkan, dan mengembalikan beberapa urutan yang konsisten ketika mereka tidak bisa.

Apakah C menawarkan sesuatu dengan fungsi serupa yang akan memungkinkan membandingkan pointer yang sewenang-wenang (dengan jenis yang sama)? Saya mencoba melihat melalui standar C11 dan tidak menemukan apa pun, tetapi pengalaman saya di C adalah urutan besarnya lebih kecil daripada di C ++, jadi saya bisa dengan mudah melewatkan sesuatu.

Angew tidak lagi bangga dengan SO
sumber
1
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew

Jawaban:

20

Pada implementasi dengan model memori datar (pada dasarnya semuanya), casting untuk uintptr_tJust Work.

(Tetapi lihat apakah perbandingan pointer ditandatangani atau tidak ditandatangani dalam 64-bit x86? Untuk diskusi apakah Anda harus memperlakukan pointer sebagai ditandatangani atau tidak, termasuk masalah pembentukan pointer di luar objek yang merupakan UB dalam C.)

Tapi sistem dengan model memori non-datar lakukan ada, dan berpikir tentang mereka dapat membantu menjelaskan situasi saat ini, seperti C ++ memiliki spesifikasi yang berbeda untuk <vs std::less.


Bagian dari titik <pada pointer ke objek yang terpisah menjadi UB di C (atau setidaknya tidak ditentukan dalam beberapa revisi C ++) adalah untuk memungkinkan mesin aneh, termasuk model memori non-flat.

Contoh terkenal adalah mode real x86-16 di mana pointer adalah segmen: offset, membentuk alamat linear 20-bit via (segment << 4) + offset. Alamat linear yang sama dapat diwakili oleh beberapa kombinasi seg: off yang berbeda.

C ++ std::lesspada pointer pada ISA aneh mungkin perlu mahal , misalnya "menormalkan" segmen: offset pada x86-16 untuk memiliki offset <= 15. Namun, tidak ada cara portabel untuk mengimplementasikan ini. Manipulasi yang diperlukan untuk menormalkan suatu uintptr_t(atau objek-representasi dari objek pointer) adalah implementasi khusus.

Tetapi bahkan pada sistem di mana C ++ std::lessharus mahal, <tidak harus. Misalnya, dengan mengasumsikan model memori "besar" di mana objek cocok dalam satu segmen, <cukup bandingkan bagian offset dan bahkan tidak repot dengan bagian segmen. (Pointer di dalam objek yang sama akan memiliki segmen yang sama, dan sebaliknya itu UB dalam C. C ++ 17 diubah menjadi hanya "tidak ditentukan", yang mungkin masih memungkinkan melompati normalisasi dan hanya membandingkan offset.) Ini mengasumsikan semua pointer ke bagian mana pun suatu benda selalu menggunakan nilai yang sama seg, tidak pernah dinormalisasi. Ini yang Anda harapkan dari ABI untuk model memori "besar" dan bukan "besar". (Lihat diskusi dalam komentar ).

(Model memori semacam itu mungkin memiliki ukuran objek maksimal 64kiB misalnya, tetapi ruang alamat total maks jauh lebih besar yang memiliki ruang untuk banyak objek berukuran maksimal tersebut. ISO C memungkinkan implementasi memiliki batas ukuran objek yang lebih rendah dari nilai maks (tidak ditandai) size_tdapat mewakili SIZE_MAX,. Misalnya, bahkan pada sistem model memori datar, GNU C membatasi ukuran objek maks PTRDIFF_MAXsehingga perhitungan ukuran dapat mengabaikan limpahan yang ditandatangani.) Lihat jawaban dan diskusi ini dalam komentar.

Jika Anda ingin mengizinkan objek yang lebih besar dari suatu segmen, Anda memerlukan model memori "besar" yang harus dikhawatirkan meluap bagian offset dari pointer ketika melakukan p++perulangan melalui array, atau ketika melakukan aritmatika pengindeksan / penunjuk. Ini mengarah ke kode yang lebih lambat di mana-mana, tetapi mungkin berarti hal itu p < qakan bekerja untuk pointer ke objek yang berbeda, karena implementasi yang menargetkan model memori "besar" biasanya akan memilih untuk menjaga semua pointer dinormalisasi sepanjang waktu. Lihat Apa yang dekat, jauh dan petunjuk besar? - beberapa kompiler C nyata untuk mode real x86 memang memiliki opsi untuk dikompilasi untuk model "besar" di mana semua pointer default ke "besar" kecuali dinyatakan sebaliknya.

Segmentasi x86 real-mode bukan satu-satunya model memori non-flat mungkin , itu hanya contoh konkret yang berguna untuk menggambarkan bagaimana itu ditangani oleh implementasi C / C ++. Dalam kehidupan nyata, implementasi diperpanjang ISO C dengan konsep farvs nearpointer, yang memungkinkan programmer untuk memilih kapan mereka bisa pergi dengan hanya menyimpan / melewati bagian offset 16-bit, relatif terhadap beberapa segmen data umum.

Tetapi implementasi ISO C murni harus memilih antara model memori kecil (semuanya kecuali kode dalam 64kiB yang sama dengan pointer 16-bit) atau besar atau besar dengan semua pointer menjadi 32-bit. Beberapa loop dapat dioptimalkan dengan menambah hanya bagian offset, tetapi objek pointer tidak dapat dioptimalkan menjadi lebih kecil.


Jika Anda tahu apa manipulasi sihir untuk implementasi yang diberikan, Anda bisa menerapkannya dalam C murni . Masalahnya adalah bahwa sistem yang berbeda menggunakan pengalamatan yang berbeda dan detailnya tidak diparameterisasi oleh makro portabel apa pun.

Atau mungkin tidak: itu mungkin melibatkan melihat sesuatu dari tabel segmen khusus atau sesuatu, misalnya seperti mode terproteksi x86, bukan mode nyata di mana bagian segmen dari alamat adalah indeks, bukan nilai yang dibiarkan bergeser. Anda dapat mengatur segmen yang tumpang tindih sebagian dalam mode terproteksi, dan bagian pemilih segmen alamat bahkan tidak perlu dipesan dalam urutan yang sama dengan alamat basis segmen yang sesuai. Mendapatkan alamat linear dari pointer seg: off dalam mode terproteksi x86 mungkin melibatkan pemanggilan sistem, jika GDT dan / atau LDT tidak dipetakan ke halaman yang dapat dibaca dalam proses Anda.

(Tentu saja OS mainstream untuk x86 menggunakan model memori datar sehingga basis segmen selalu 0 (kecuali untuk penggunaan fsatau gssegmen penyimpanan thread-lokal ), dan hanya bagian "offset" 32-bit atau 64-bit yang digunakan sebagai penunjuk .)

Anda dapat secara manual menambahkan kode untuk berbagai platform tertentu, misalnya secara default menganggap datar, atau #ifdefsesuatu untuk mendeteksi mode real x86 dan membaginya uintptr_tmenjadi dua bagian 16-bit untuk seg -= off>>4; off &= 0xf;kemudian menggabungkan bagian-bagian itu kembali ke angka 32-bit.

Peter Cordes
sumber
Mengapa itu menjadi UB jika segmennya tidak sama?
Acorn
@ Corn: Dimaksudkan untuk mengatakan sebaliknya; tetap. pointer ke objek yang sama akan memiliki segmen yang sama, selain UB.
Peter Cordes
Tetapi mengapa Anda berpikir itu adalah UB? (logika terbalik atau tidak, sebenarnya saya juga tidak memperhatikan)
Acorn
p < qapakah UB dalam C jika mereka menunjuk ke objek yang berbeda, bukan? Saya tahu p - qitu.
Peter Cordes
1
@Acorn: Bagaimanapun, saya tidak melihat mekanisme yang akan menghasilkan alias (berbeda seg: off, alamat linear yang sama) dalam sebuah program tanpa UB. Jadi bukan berarti kompiler harus keluar dari jalannya untuk menghindari itu; setiap akses ke suatu objek menggunakan segnilai objek itu dan offset yang> = offset dalam segmen tempat objek itu dimulai. C membuatnya UB untuk melakukan banyak hal antara pointer ke objek yang berbeda, termasuk hal-hal seperti tmp = a-bdan kemudian b[tmp]mengakses a[0]. Diskusi tentang aliasing pointer tersegmentasi ini adalah contoh yang baik mengapa pilihan-desain itu masuk akal.
Peter Cordes
17

Saya pernah mencoba untuk menemukan cara mengatasi ini dan saya memang menemukan solusi yang berfungsi untuk objek yang tumpang tindih dan dalam kebanyakan kasus dengan asumsi kompiler melakukan hal yang "biasa".

Pertama-tama Anda dapat menerapkan saran dalam Bagaimana menerapkan memmove dalam standar C tanpa salinan perantara? dan kemudian jika itu tidak berhasil dilemparkan ke uintptr(tipe pembungkus untuk salah satu uintptr_tatau unsigned long longtergantung pada apakah uintptr_ttersedia) dan mendapatkan hasil yang paling akurat (walaupun mungkin tidak masalah):

#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;
}
SS Anne
sumber
5

Apakah C menawarkan sesuatu dengan fungsi serupa yang akan memungkinkan membandingkan pointer yang aman secara aman.

Tidak


Pertama mari kita hanya mempertimbangkan pointer objek . Pointer fungsi membawa seluruh rangkaian masalah lainnya.

2 pointer p1, p2dapat memiliki penyandian yang berbeda dan menunjuk ke alamat yang sama sehingga p1 == p2meskipun memcmp(&p1, &p2, sizeof p1)tidak 0. Arsitektur seperti itu jarang terjadi.

Namun konversi dari pointer ini ke uintptr_ttidak memerlukan hasil integer yang sama yang mengarah ke (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 itu sendiri adalah kode hukum yang baik, oleh mungkin tidak menyediakan fungsionalitas yang diharapkan.


Jika kode benar-benar perlu membandingkan pointer yang tidak terkait, bentuk fungsi pembantu less(const void *p1, const void *p2)dan lakukan kode spesifik platform di sana.

Mungkin:

// 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 
}
chux - Pasang kembali Monica
sumber