web-dev-qa-db-ja.com

関数ポインタへの関数の割り当て、const引数の正確性?

私はC++の基礎とOOPを私の大学で学習しています。関数を割り当てるときに関数ポインターがどのように機能するかは、100%わかりません。次のコードに遭遇しました。

void mystery7(int a, const double b) { cout << "mystery7" << endl; }
const int mystery8(int a, double b) { cout << "mystery8" << endl; }

int main() {
    void(*p1)(int, double) = mystery7;            /* No error! */
    void(*p2)(int, const double) = mystery7;
    const int(*p3)(int, double) = mystery8;
    const int(*p4)(const int, double) = mystery8; /* No error! */
}

私の理解から、p2およびp3関数パラメーターの型が一致し、定数が正しいため、割り当ては問題ありません。しかし、なぜp1およびp4割り当てが失敗しましたか? const double/intを非const double/intに一致させることは違法ではないでしょうか?

16
edwardlam0328

C++標準に準拠(C++ 17、16.1オーバーロード可能宣言)

(3.4)— constおよび/またはvolatileの有無のみが異なるパラメーター宣言は同等です。つまり、宣言、定義、または呼び出される関数を決定するときに、各パラメーター型のconstおよびvolatile型指定子は無視されます。

したがって、関数タイプを決定するプロセスでは、たとえば以下の関数宣言の2番目のパラメーターの修飾子constは破棄されます。

_void mystery7(int a, const double b);
_

関数のタイプはvoid( int, double )です。

次の関数宣言も考慮してください

_void f( const int * const p );
_

次の宣言に相当します

_void f( const int * p );
_

パラメーターを定数にするのは2番目のconstです(つまり、ポインター自体を関数内で再割り当てできない定数オブジェクトとして宣言します)。最初のconstは、ポインターのタイプを定義します。破棄されません。

C++標準では「const参照」という用語が使用されていますが、参照自体はポインタと反対にできないことに注意してください。それは次の宣言です

_int & const x = initializer;
_

間違っている。

この宣言は

_int * const x = initializer;
_

正しく、定数ポインターを宣言します。

18

値によって渡される関数引数には特別な規則があります。

それらのconstは、関数内での使用に影響を及ぼしますが(事故を防ぐため)、基本的には署名では無視されます。これは、値によって渡されるオブジェクトのconstnessが、呼び出しサイトの元のコピー元オブジェクトにまったく影響を与えないためです。

それはあなたが見ているものです。

(個人的には、この設計上の決定は間違いだったと思います。混乱し、不必要です!しかし、それはそれです。それは、void foo(T arg[5]);を静かにvoid foo(T* arg);に変更する同じ箇所から来ていることに注意してください、だから私たちが対処しなければならないことはすでにたくさんあります!)

ただし、これはそのような引数の型のanyconstを単に消去するだけではないことを思い出してください。 _int* const_ではポインターはconstですが、_int const*_(または_const int*_)ではポインターはconstではなくconst事。最初の例のみがポインタ自体のconstnessに関連しており、削除されます。


_[dcl.fct]/5_ 関数のタイプは、次のルールを使用して決定されます。各パラメーターのタイプ(関数パラメーターパックを含む)は、それ自体のdecl-specifier-seqおよび宣言子から決定されます。各パラメーターのタイプを判別した後、タイプ「Tの配列」または関数タイプTのパラメーターは、「Tへのポインター」になるように調整されます。パラメータタイプのリストを生成した後、関数タイプを形成するときに、パラメータタイプを変更するすべてのトップレベルcv-qualifiersが削除されます。変換されたパラメータータイプの結果のリストと、省略記号または関数パラメーターパックの有無は、関数のparameter-type-listです。 [注:この変換は、パラメーターのタイプには影響しません。 たとえば、int(*)(const int p, decltype(p)*)int(*)(int, const int*)は同じ型です。— end note ]

関数の引数にconst修飾子を追加または削除することが重大なバグである状況があります。これは、引数ポインタによるを渡したときに発生します。

これは、問題が発生する可能性がある簡単な例です。このコードはCで壊れています:

_#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// char * strncpy ( char * destination, const char * source, size_t num );

/* Undeclare the macro required by the C standard, to get a function name that
 * we can assign to a pointer:
 */
#undef strncpy

// The correct declaration:
char* (*const fp1)(char*, const char*, size_t) = strncpy;
// Changing const char* to char* will give a warning:
char* (*const fp2)(char*, char*, size_t) = strncpy;
// Adding a const qualifier is actually dangerous:
char* (*const fp3)(const char*, const char*, size_t) = strncpy;

const char* const unmodifiable = "hello, world!";

int main(void)
{
  // This is undefined behavior:
  fp3( unmodifiable, "Whoops!", sizeof(unmodifiable) );

  fputs( unmodifiable, stdout );
  return EXIT_SUCCESS;
}
_

ここでの問題は_fp3_にあります。これは、2つの_const char*_引数を受け入れる関数へのポインターです。ただし、これは標準ライブラリコールstrncpy()¹を指しており、その最初の引数はmodifyであるバッファです。つまり、fp3( dest, src, length )destが指すデータを変更しないことを約束する型を持っていますが、引数をstrncpy()に渡し、そのデータを変更します!これは、関数の型シグネチャを変更したためにのみ可能です。

文字列定数を変更しようとすることは未定義の動作であり、実際にはプログラムにstrncpy( "hello, world!", "Whoops!", sizeof("hello, world!") )を呼び出すように指示しました-私がテストしたいくつかの異なるコンパイラーでは、実行時に静かに失敗します。

最新のCコンパイラは、_fp1_への割り当てを許可する必要がありますが、_fp2_または_fp3_のどちらかを使用して自分の足で撃っているという警告を出します。 C++では、_fp2_および_fp3_行は、_reinterpret_cast_なしではコンパイルできません。明示的なキャストを追加すると、コンパイラはあなたが何をしているのかを知っていると想定し、警告を消しますが、プログラムは未定義の動作のために失敗します。

_const auto fp2 =
  reinterpret_cast<char*(*)(char*, char*, size_t)>(strncpy);
// Adding a const qualifier is actually dangerous:
const auto fp3 =
  reinterpret_cast<char*(*)(const char*, const char*, size_t)>(strncpy);
_

コンパイラーがそれらのコピーを作成するため、これは値で渡される引数では発生しません。値constで渡されたパラメーターをマークすることは、関数が一時コピーを変更する必要がないことを意味します。たとえば、標準ライブラリが内部でchar* strncpy( char* const dest, const char* const src, const size_t n )を宣言している場合、K&Rイディオム_*dest++ = *src++;_を使用できません。これにより、constと宣言した関数の引数の一時コピーが変更されます。これはプログラムの他の部分には影響しないので、Cは、関数プロトタイプまたは関数ポインターのようなconst修飾子を追加または削除してもかまいません。通常、これらは実装の詳細であるため、ヘッダーファイルのパブリックインターフェイスの一部にはしません。

I正しいシグネチャを持つ既知の関数の例としてstrncpy()を使用していますが、一般的には非推奨です。

1
Davislor