C
C++

標準ヘッダのオーバーライド

More than 1 year has passed since last update.

はじめに

概要

ご存じの通りC/C++では、各種マクロ、プロトタイプ、構造体定義、またC++の場合だと更にクラスやテンプレートなどをヘッダファイルに分離し、#includeによって取り込みます。

それでは、既に標準ヘッダにあるものと同名のヘッダをつくれば、中身をオーバーライドできるのではないか…? という疑問が出たため、検証してみました

環境

  • Windows10/WSL ( Ubuntu 16.04.2 LTS相当 )
  • gcc/g++ 5.4.0 ( パッケージ版 )

検証内容

試すこと

  • 標準関数の1つを差し替えて、意図通りの挙動になるかどうかを見る
  • 他の標準関数も同時に使用し、挙動が変わらないことを見る

この2番目を入れているのは、ちゃんと標準ヘッダの代替として使えるかどうかを確認するためです。中身を差し替えることができました、でも標準ヘッダの機能が失われました、では意味がないと考えました。

サンプルコード

今回用意したコードは次の通りです。2つ用意するのは面倒だったのでC/C++両用にして、ヘッダを取り込む部分だけ変えています。なのでまんまCなのはご勘弁を。
※こういう時は__cplusplusマクロが使えますね。

実装としては、良くあるhelloworldを2通り出力するものです。

hello.c
#ifdef __cplusplus
# include <iostream>
# include <cstdio>
using std::fputs;
using std::putchar;
#else
# include <stdio.h>
#endif

int main() {
    const static char hello[]="Hello, world!\n";

#ifdef __cplusplus
    std::cout << "helloworld for C++" << std::endl;
#else
    printf("helloworld for C\n");
#endif

    for ( const char *p=&hello[0]; *p; p++ )
        putchar(*p);

    fputs(hello, stdout);
}

なお、実行結果は次の通りです。

$ gcc -o hello-c hello.c; ./hello-c
helloworld for C
Hello, world!
Hello, world!
$ g++ -o hello-c++ hello.c; ./hello-c++
helloworld for C++
Hello, world!
Hello, world!

先頭行を追加したのは、C/C++としてコンパイルされていることが分かるように、です。まあ一応念のためということで。

差し替えヘッダ

では、ヘッダを差し替えるわけですが、

  • 標準のstdio.hあるいはcstdioを差し替える
  • putchar()を、「大文字に変換したうえで出力する」という挙動に変える

ということで試してみます。
具体的には、次のようなヘッダを用意します。

stdio.h
#ifndef H_OVERRIDE_TEST_STDIO_H
#define H_OVERRIDE_TEST_STDIO_H

#include <ctype.h>
#include "/usr/include/stdio.h"
#define putchar(c) putc(toupper(c),stdout)

#endif
cstdio
#ifndef H_OVERRIDE_TEST_CSTDIO
#define H_OVERRIDE_TEST_CSTDIO

#include <cctype>
#include "/usr/include/c++/5/cstdio"

namespace std {
    inline int putchar_override(int c) {
        return std::putc(std::toupper(c),stdout);
    }
}

#define putchar putchar_override

#endif

差し替えは#defineマクロにより行います。
が、その前にオリジナルのヘッダを#includeしておくことで、差し替える部分以外がそのまま使えるようにします。
なお、C++の方がより込み入った作りになっているのは、namespaceが絡んでもちゃんと対処できるように、と考慮した結果です。つまりCのようなマクロとしての関数の差し替えではなくて、C++上で定義したinline関数へのシンボルの差し替え、という扱いにしているわけです。
※まあ完璧に対処できているかどうかは分かりませんが

差し替え実行結果

gcc/g++の場合 ( というか、様々なコンパイラでポピュラーだと思いますが )、-Iオプションによってヘッダの検索場所を追加できます。
上で作ったヘッダをカレントディレクトリに置いて、差し替えを試してみます。
※一応ヘッダの干渉があるかも知れないので、一方を試す時は、もう一方用のヘッダはファイル名を変えておきます。

$ gcc -I. -o hello-c hello.c; ./hello-c
helloworld for C
HELLO, WORLD!
Hello, world!
$ g++ -I. -o hello-c++ hello.c; ./hello-c++
helloworld for C++
HELLO, WORLD!
Hello, world!

いずれも、putchar()の処理のところが、差し替え版ヘッダで意図した「大文字に変換した上で出力する」になっていることと、fputs()の挙動はそのままであることが確認できます。

追加検証~標準のincludeパス~

さて、-Iオプションを使用することで、標準ヘッダをオーバーライドできることが分かったのですが、いっそ-Iなしでも差し替え版ヘッダが使われるようにイタズラできないでしょうか。
というのも、パッケージ版gcc/g++同梱のヘッダは/usr/includeなり/usr/include/c++なりに格納されますが、それとは別に/usr/local/includeというヘッダ置き場も用意されているからです。

というわけで試してみます。

$ ls /usr/local/include
stdio.h
$ gcc -o hello-c hello.c; ./hello-c
helloworld for C
HELLO, WORLD!
Hello, world!
$ ls /usr/local/include
cstdio
$ g++ -o hello-c++ hello.c; ./hello-c++
helloworld for C++
Hello, world!
Hello, world!

/usr/local/includeに差し替え版ヘッダを置いた場合、gccでは-I無しでもオーバーライドされましたが、g++の場合はオーバーライドされませんでした。

面白いことにg++の場合、/usr/local/includeを明示的に-Iオプションで指定しても、やはりオーバーライドされません。

$ g++ -I/usr/local/include -o hello-c++ hello.c; ./hello-c++
helloworld for C++
Hello, world!
Hello, world!

それでちょっと調べたところ、まず、コンパイラがヘッダを探索するディレクトリのリストは、コンパイル時に-vオプションを指定することで見ることができると分かりました。
で、各ディレクトリには優先順位があって、それが某かの影響を与えている、ということのようです。

$ gcc -v -o hello-c hello.c
…(略)…
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
…(略)…
$ g++ -v -o hello-c++ hello.c
…(略)…
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
…(略)…

この結果から分かる/推測できることは、次の通りになります。なかなかややこしいですね。

  • /usr/local/includeはデフォルトでヘッダ検索対象なので、-Iを指定しなくとも、同ディレクトリにあるヘッダが使用できる
  • 優先順位の関係上、同名のヘッダstdio.hは、標準の/usr/includeにあるものではなく、/usr/local/includeの方が使用される
  • しかしcstdioは標準の/usr/include/c++/5の方が優先順位が高いため/usr/local/includeの方は使用されない
  • 対象にないディレクトリ ( 今回の検証だと. ) を-Iで指定すると、デフォルトよりも優先される
  • しかし既にデフォルトの対象に含まれる/usr/local/includeは、-Iオプションで明示しても、優先順位が覆らないと推測できる

まとめ

  • gcc/g++の場合、標準ヘッダと同名のファイルを作り、-Iでそのファイルのあるディレクトリを指定することで、標準ヘッダをオーバーライドできる
  • /usr/includeにあるC用標準ヘッダの場合、/usr/local/includeに同名のファイルを置けば、-Iなしでもオーバーライドされる
  • /usr/include/c++にあるC++用標準ヘッダの場合、/usr/local/includeに同名のファイル置くと、むしろ-Iをつけてもオーバーライドされない

まあ、標準ヘッダをオーバーライドする需要はそうないと思いますが、目的のプログラムの実装はそのままに、標準のライブラリの挙動を少し弄りたい場合に使えるかも知れませんね。

補足

頂いたコメントを見るに、そういう需要は確かにあるようですね。

drabさんに教えて頂いた#include_next(gcc拡張)については、gccのドキュメントのWrpper Headersの章にありました。

機能としては、

It simply looks for the file named, starting with the directory in the search path after the one where the current file was found.

要約: #includeのディレクトリ探索で見つかったところから、より低優先度のディレクトリを探索する ( そして見つかったファイルを取り込む )

ということのようです。
そうすると、差し替えヘッダで挙げた、例えばstdio.hであれば、次のように書き換えられます。生のファイル名を消せるわけで、こっちの方が良いですね。

stdio.h
#ifndef H_OVERRIDE_TEST_STDIO_H
#define H_OVERRIDE_TEST_STDIO_H

#include <ctype.h>
#include_next <stdio.h>
#define putchar(c) putc(toupper(c),stdout)

#endif