C'nin C ++ 'dan std :: less karşılığı var mı?


26

Kısa süre önce ve p < qne zaman farklı nesneler / diziler işaretçiler C yapmak tanımsız davranışı hakkında bir soru cevap . Bu beni düşündürdü: C ++ bu durumda aynı (tanımsız) davranışa sahiptir , ancak aynı zamanda işaretçilerin karşılaştırılabildiği zaman aynı şeyi döndürmesi ve yapamadıkları zaman tutarlı bir sıralama döndürmesi garanti edilen standart kütüphane şablonunu sunar .pq<std::less<

C, keyfi işaretçileri (aynı türle) güvenli bir şekilde karşılaştırmaya izin verecek benzer işlevselliğe sahip bir şey sunuyor mu? C11 standardına bakmaya çalıştım ve hiçbir şey bulamadım, ancak C'deki deneyimim C ++ 'dan daha küçük büyüklükteki siparişler, bu yüzden kolayca bir şey kaçırmış olabilirdim.


1
Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Samuel Liew

Yanıtlar:


20

Düz bellek modeli (temelde her şey) ile yapılan uygulamalarda, uintptr_tJust Work'e yayınlanacak.

(Ancak, işaretçiler karşılaştırmaları 64 bit x 86'da imzalanmalı veya imzalanmamalı mı? C'de UB olan nesnelerin dışında işaretçiler oluşturma sorunları da dahil olmak üzere, işaretleyicilere imzalı olarak davranılıp davranılmayacağını tartışmak için bkz.)

Ancak düz olmayan bellek modelleri olan sistemler mevcuttur ve bunlar hakkında düşünmek, C ++ <vs. için farklı özelliklere sahip olan mevcut durumu açıklamaya yardımcı olabilir std::less.


Alanına bir kısmı <C UB (ya da C ++ olarak gözden geçirilmesinde, en azından belirtilmemiş) olan ayrı nesneler için işaretçiler düz olmayan bellek modelleri dahil olmak üzere garip makineler için sağlamaktır.

İyi bilinen bir örnek, işaretçilerin segmentli olduğu x86-16 gerçek modudur: ofset, üzerinden 20 bit doğrusal adres oluşturur (segment << 4) + offset. Aynı doğrusal adres, birden çok farklı seg: off kombinasyonu ile temsil edilebilir.

std::lessGarip ISA'larda işaretçiler üzerinde C ++ pahalı olabilir , örneğin bir segmenti "normalleştirin": ofset <= 15 olması için x86-16'da ofset. Ancak, bunu uygulamanın taşınabilir bir yolu yoktur . Bir uintptr_t(veya bir işaretçi nesnesinin nesne gösterimi) normalleştirmek için gereken manipülasyon uygulamaya özgüdür.

Ancak C ++ ' std::lessnin pahalı olması gereken sistemlerde bile , <olması gerekmez. Örneğin, bir nesnenin bir segment içine sığdığı bir "büyük" bellek modeli varsayarsak <, ofset kısmını karşılaştırabilir ve hatta segment kısmı ile uğraşmayabilir. (Aynı nesnenin içindeki işaretçiler aynı segmente sahiptir ve aksi takdirde C'deki UB'dir. C ++ 17 yalnızca "belirtilmemiş" olarak değiştirilmiştir, bu da normalleştirmeyi atlamaya ve sadece ofsetleri karşılaştırmaya izin verebilir.) nesnenin her zaman aynı segdeğeri kullanır, asla normalleştirmez. Bu, bir ABI'nin "büyük" bellek modelinin aksine "büyük" olmasını gerektirmesini beklersiniz. ( Yorumlardaki tartışmaya bakın ).

(Böyle bir bellek modeli, maksimum 64kiB nesne boyutuna sahip olabilir, ancak bu kadar çok sayıda maksimum nesne için yer olan çok daha büyük bir maksimum toplam adres alanı olabilir. ISO C, uygulamaların, nesne boyutunda, maksimum değer (imzasız) size_t, SIZE_MAXörneğin düz bellek model sistemlerinde bile, GNU C maksimum nesne boyutunu sınırlar, PTRDIFF_MAXböylece boyut hesaplaması imzalı taşmayı göz ardı edebilir.) Bu yanıta ve tartışmaya yorumlarda bakın.

Bir segmentten daha büyük nesnelere izin vermek istiyorsanız, p++bir dizi boyunca döngü gerçekleştirirken veya indeksleme / işaretçi aritmetiği yaparken işaretçinin ofset kısmının taşması konusunda endişelenmesi gereken "büyük" bir bellek modeline ihtiyacınız vardır . Bu, her yerde daha yavaş kodlara yol açar, ancak muhtemelen p < qfarklı nesnelere işaretçiler için işe yarayacağı anlamına gelir , çünkü "büyük" bir bellek modelini hedefleyen bir uygulama normalde tüm işaretçileri her zaman normalleştirmeyi seçer. Bkz uzak ve büyük işaretçileri, yakın nelerdir? - x86 gerçek modu için bazı gerçek C derleyicileri, aksi belirtilmedikçe tüm işaretçilerin varsayılan olarak "dev" olarak ayarlandığı "dev" model için derleme seçeneğine sahipti.

x86 gerçek mod segmentasyonu mümkün olan tek düz bellek modeli değildir, C / C ++ uygulamaları tarafından nasıl ele alındığını göstermek için sadece yararlı bir somut örnektir. Gerçek hayatta uygulamalar, ISO C'yi işaretçiler farve nearişaretçi kavramıyla genişleterek , programcıların bazı ortak veri segmentlerine göre sadece 16 bitlik ofset parçasını depolamak / geçirmek için ne zaman uzaklaşabileceklerini seçmelerine olanak tanır.

Ancak saf bir ISO C uygulaması, küçük bir bellek modeli (16 bit işaretçilerle aynı 64kiB'deki kod hariç her şey) veya tüm işaretçiler 32 bit olacak şekilde büyük veya çok büyük arasında seçim yapmak zorunda kalacaktır. Bazı döngüler, yalnızca ofset kısmını artırarak optimize edebilir, ancak işaretçi nesneleri daha küçük olacak şekilde optimize edilemez.


Herhangi bir uygulama için sihirli manipülasyonun ne olduğunu biliyorsan, onu saf C'de uygulayabilirsin . Sorun, farklı sistemlerin farklı adresleme kullanması ve ayrıntıların herhangi bir taşınabilir makro tarafından parametreleştirilmemesidir.

Ya da olmayabilir: özel bir segment tablosundan ya da adresin segment bölümünün bir dizin olduğu bir gerçek dizin yerine gerçek mod yerine x86 korumalı mod gibi bir şeyin aranmasını içerebilir. Korumalı modda kısmen çakışan segmentler oluşturabilirsiniz ve adreslerin segment seçici bölümleri karşılık gelen segment temel adresleriyle aynı sırada sipariş edilmeyebilir. GDT ve / veya LDT, işleminizdeki okunabilir sayfalarla eşlenmemişse, x86 korumalı modda bir seg: off işaretçisinden doğrusal adres almak bir sistem çağrısı içerebilir.

(Tabii ki x86 için ana işletim sistemleri düz bir bellek modeli kullanır, böylece segment tabanı her zaman 0 olur (iş parçacığı fsveya yerel segment kullanımı hariç gs) ve işaretçi olarak yalnızca 32 bit veya 64 bit "ofset" kısmı kullanılır .)

Çeşitli belirli platformlar için manuel olarak kod ekleyebilirsiniz, örneğin varsayılan olarak düz olduğunu varsayalım veya #ifdefx86 gerçek modunu algılayacak ve uintptr_t16 bit yarıya bölünecek bir şey seg -= off>>4; off &= 0xf;daha sonra 32 bit bir sayıya geri birleştirebilirsiniz.


Segment eşit değilse neden UB olur?
Acorn

@Acorn: Bunun tersini söylemek istedim; sabit. aynı nesneye işaretçiler aynı segmente, diğer UB'ye sahip olacaktır.
Peter Cordes

Ama neden her durumda UB olduğunu düşünüyorsun? (ters mantık ya da değil, aslında ben de fark etmedim)
Acorn

p < qUB farklı nesnelere işaret ediyorsa C değil mi? Biliyorum p - q.
Peter Cordes

1
@Acorn: Her neyse, UB olmayan bir programda takma adlar (farklı seg: off, aynı doğrusal adres) oluşturacak bir mekanizma görmüyorum. Yani derleyici bundan kaçınmak için kendi yolundan çıkmak zorunda değil; bir nesneye her erişim, o nesnenin segdeğerini ve o nesnenin başladığı segment içindeki ofset değerini = = kullanır . C, UB'yi farklı nesneler için işaretçiler arasında tmp = a-bve daha sonra erişilenler de dahil olmak üzere birçok şey b[tmp]yapmayı sağlar a[0]. Bölümlenmiş işaretçi takma adı ile ilgili bu tartışma, tasarım seçiminin neden anlamlı olduğuna iyi bir örnektir.
Peter Cordes

17

Bir zamanlar bunun etrafında bir yol bulmaya çalıştım ve üst üste binen nesneler için çalışan bir çözüm buldum ve çoğu durumda derleyicinin "olağan" bir şey yaptığını varsayarak.

Öncelikle , ara kopya olmadan standart C'de memmove nasıl uygulanır? ve daha sonra bu, yayınlanmayacaksa uintptr(ya biri için ya uintptr_tda mevcut unsigned long longolup olmadığına bağlı olarak bir sarıcı türü uintptr_t) ve en olası doğru sonucu elde ederse (muhtemelen önemli olmayacak olsa da):

#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, keyfi işaretçileri güvenli bir şekilde karşılaştırmaya izin verecek benzer işlevselliğe sahip bir şey sunuyor mu?

Hayır


Önce sadece nesne işaretleyicilerini ele alalım . İşlev işaretçileri başka bir dizi endişe getirir.

2 işaretçi p1, p2farklı kodlamalara sahip olabilir ve aynı adresi gösterebilir . 0 olmamasına p1 == p2rağmen memcmp(&p1, &p2, sizeof p1). Bu tür mimariler nadirdir.

Ancak bu işaretçinin dönüştürülmesi uintptr_tiçin aynı tamsayı sonucunu gerektirmez (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 kendisi iyi bir yasal koddur, işlevsellik umuduyla olmayabilir.


Kodun gerçekten alakasız işaretçileri karşılaştırması gerekiyorsa, bir yardımcı işlev oluşturun less(const void *p1, const void *p2)ve orada platforma özel kod gerçekleştirin.

Belki:

// 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 
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.