CにはC ++のstd :: lessと同等のものがありますか?


26

私は最近やっての未定義の動作に質問に答えるたp < qときCにpし、q異なるオブジェクト/配列へのポインタです。それは私に考えさせました:C ++は<この場合と同じ(未定義)動作を持っていますが、ポインターが比較できる場合std::lessと同じものを返すことが保証され<、できない場合は一貫した順序を返す標準ライブラリテンプレートも提供します。

Cは、(同じ型への)任意のポインタを安全に比較できるようにする同様の機能を持つものを提供していますか?C11標準を調べても何も見つかりませんでしたが、Cでの経験はC ++よりも桁違いに小さいため、簡単に何かを見落としていた可能性があります。


1
コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Samuel Liew

回答:


20

フラットメモリモデル(基本的にはすべて)の実装では、キャストするuintptr_tとJust Workになります。

(しかし、CのUBであるオブジェクトの外でポインターを形成する問題を含め、ポインターを符号付きとして扱うべきかどうかについては、64ビットx86でポインター比較を符号付きまたは符号なしにする必要があるかを参照してください。)

しかし、非フラットなメモリモデルを持つシステムが存在しませんし、それらについて考えることのために異なるスペック持つ++ Cのように、現在の状況を説明するのに役立つことができます<対をstd::less


<CでUBである(または少なくとも一部のC ++リビジョンでは指定されていない)個別のオブジェクトへのポインターの要点の1つは、非フラットメモリモデルを含む奇妙なマシンを許可することです。

よく知られている例は、x86-16リアルモードで、ポインターはセグメント:オフセットで、を介して20ビットの線形アドレスを形成します(segment << 4) + offset。同じ線形アドレスを複数の異なるseg:offの組み合わせで表すことができます。

std::less奇妙なISA上のポインターのC ++ はコストがかかる可能性があります。たとえば、x86-16でセグメント:オフセットを「正規化」してオフセット<= 15にする必要があります。ただし、これを実装するポータブルな方法はありません。 a uintptr_t(またはポインタオブジェクトのオブジェクト表現)を正規化するために必要な操作は、実装固有です。

しかし、C ++ std::lessが高価で<なければならないシステムであっても、そうである必要はありません。たとえば、オブジェクトが1つのセグメント内に収まる「大きな」メモリモデルを想定する<と、オフセットパーツを比較するだけで、セグメントパーツを気にする必要さえありません。(同じオブジェクト内のポインターは同じセグメントを持ち、それ以外の場合はCのUBです。C++ 17は単に「未指定」に変更されました。これにより、正規化のスキップとオフセットの比較だけが可能になる場合があります。)これは、任意の部分へのすべてのポインターを想定しています。オブジェクトは常に同じseg値を使用し、正規化することはありません。これは、「巨大な」メモリモデルではなく「大きな」メモリモデルにABIが必要とするものです。(コメントの議論を参照)。

(たとえば、このようなメモリモデルの最大オブジェクトサイズは64kiBですが、そのような最大サイズのオブジェクトの多くに対応できる十分な最大合計アドレススペースがあります。ISOCでは、実装でオブジェクトサイズの制限を最大値(符号なし)size_tはを表すことができSIZE_MAXます。たとえば、フラットメモリモデルシステムでも、GNU Cは最大オブジェクトサイズを制限しているPTRDIFF_MAXため、サイズ計算で符号付きオーバーフローを無視できます。この回答とコメントでの議論を参照してください。

セグメントよりも大きいオブジェクトを許可する場合はp++、配列をループするとき、またはインデックス付け/ポインター演算を行うときに、ポインターのオフセット部分のオーバーフローを心配する必要がある「巨大な」メモリーモデルが必要です。これはどこでもコードが遅くなりますがp < q、「巨大な」メモリモデルをターゲットとする実装は通常、常にすべてのポインタを正規化することを選択するため、異なるオブジェクトへのポインタで機能する可能性があります。近く、遠く、巨大なポインタは何ですか?を参照してください-一部のx86リアルモード用の実際のCコンパイラには、「巨大」モデル用にコンパイルするオプションがあり、他に宣言されていない限り、すべてのポインタはデフォルトで「巨大」になります。

x86リアルモードセグメンテーションは、唯一の非フラットメモリモデルではありません。これは、C / C ++実装による処理方法を説明するのに役立つ具体例にすぎません。実際の実装では、farvs。nearポインタの概念を使用してISO Cを拡張し、一般的なデータセグメントを基準にして、プログラマが16ビットのオフセット部分を単に格納/渡すだけで済むようにするタイミングを選択できるようにしました。

しかし、純粋なISO Cの実装では、小さなメモリモデル(16ビットポインターを持つ同じ64kiBのコードを除くすべて)、またはすべてのポインターが32ビットである大きなメモリモデルまたは巨大なモデルのいずれかを選択する必要があります。一部のループは、オフセット部分のみをインクリメントすることで最適化できますが、ポインターオブジェクトを小さくするように最適化できませんでした。


特定の実装に対する魔法の操作がわかっている場合は、純粋なCで実装できます。問題は、異なるシステムが異なるアドレッシングを使用し、詳細が移植可能なマクロによってパラメーター化されないことです。

または、そうでない場合もあります。たとえば、アドレスのセグメント部分がインデックスであり、左シフトされる値ではないリアルモードではなく、x86プロテクトモードなど、特別なセグメントテーブルなどから何かを検索する場合があります。プロテクトモードで部分的に重複するセグメントを設定できます。アドレスのセグメントセレクタ部分は、対応するセグメントのベースアドレスと同じ順序で並べられるとは限りません。x86プロテクトモードでseg:offポインターから線形アドレスを取得するには、GDTやLDTがプロセス内の読み取り可能なページにマップされていない場合、システムコールが必要になる場合があります。

(もちろん、x86の主流のOSはフラットメモリモデルを使用しているため、セグメントベースは常に0(fsまたはgsセグメントを使用するスレッドローカルストレージを除く)であり、32ビットまたは64ビットの「オフセット」部分のみがポインターとして使用されます。)

さまざまな特定のプラットフォーム用のコードを手動で追加できます。たとえば、デフォルトでフラットと想定する#ifdefか、x86リアルモードを検出uintptr_tし、16ビットの半分に分割してseg -= off>>4; off &= 0xf;、それらの部分を32ビットの数値に結合します。


セグメントが等しくない場合、なぜそれがUBになるのですか?
ドングリ

@Acorn:逆の言い方をすることを意味します。修繕。同じオブジェクトへのポインターは同じセグメントを持ち、それ以外の場合はUBになります。
Peter Cordes

しかし、なぜそれがUBだと思いますか?(逆論理かどうか、実際には気づかなかった)
Acorn

p < qCのUBは、別のオブジェクトを指しているのではないでしょうか。私は知っp - qています。
Peter Cordes

1
@Acorn:とにかく、UBなしのプログラムでエイリアス(異なるseg:off、同じ線形アドレス)を生成するメカニズムは見当たりません。したがって、それを回避するためにコンパイラが邪魔にならないようにする必要はありません。オブジェクトへのすべてのアクセスは、そのオブジェクトのseg値と、そのオブジェクトが開始するセグメント内のオフセット以上のオフセットを使用します。Cは、さまざまなオブジェクトへのポインタの間にあるような多くのことをUBにしてtmp = a-bb[tmp]にアクセスしてからにアクセスさせますa[0]。セグメント化されたポインターのエイリアシングに関するこの議論は、その設計選択が理にかなっている理由の良い例です。
Peter Cordes

17

私はかつてこれを回避する方法を見つけようとしましたが、重複するオブジェクトに対して機能する解決策を見つけました。他のほとんどの場合、コンパイラが「通常の」ことを行うと想定しています。

最初に、中間コピーなしで標準Cにmemmoveを実装する方法の提案を実装できますか?そして、それがキャストに機能しない場合uintptr(いずれかのラッパータイプ、uintptr_tまたは使用可能unsigned long longかどうかに応じたラッパータイプuintptr_t)、最も可能性の高い正確な結果が得られます(とにかく問題はないでしょう)。

#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は、任意のポインターを安全に比較できるようにする同様の機能を持つものを提供しますか?

番号


まず、オブジェクトポインタのみを考慮します関数ポインタは、その他の一連の問題を引き起こします。

2つのポインタはp1, p2そう同じアドレスに異なるエンコーディング点を有することができるp1 == p2にもかかわらずmemcmp(&p1, &p2, sizeof p1)0このようなアーキテクチャはまれではありません。

ただし、これらのポインタをに変換uintptr_tする場合、同じ整数の結果が必要になるわけではありません(uintptr_t)p1 != (uinptr_t)p2

(uintptr_t)p1 < (uinptr_t)p2 それ自体は十分に合法なコードであり、期待される機能を提供しない可能性があります。


コードが本当に関連のないポインタを比較する必要がある場合は、ヘルパー関数less(const void *p1, const void *p2)を作成し、そこでプラットフォーム固有のコードを実行します。

おそらく:

// 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 
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.