出現するconstと消失するconst

この記事はC++11 Advent Calendar 2011 : ATNDの参加記事です。

僕はあんまりC++11を使いこんでないため、C++11のノウハウが無いので、C++11にも関連するようなC++の面白い記事を翻訳していつでも自分が読めるようにしておいてお茶をにごすことにしますね。

http://cpp-next.com/というC++関連のレベルの高い記事を掲載しているブログがあります。
今回はそこからhttp://cpp-next.com/archive/2011/04/appearing-and-disappearing-consts-in-c/という記事を選びました。
あと、翻訳の質は保証されません。ミスは指摘していただけると嬉しいです。
この記事はC++11の規格採択以前に公開されたものなので、C++11の事をC++0xと呼んでいます。文中では適宜C++11と読み替えてください。

"Appearing and Disappearing consts in C++"

C++Nextは幸運にもScott Meyersの許可を得て彼のこの記事を再版することが出来ました。
ありがとう、Scott!

もしあなたがC++で"int i;"と書いたなら、iの型は明らかにintです。もし"const int i;"と書いたなら、同様にiの型はconst intになるでしょう。多くの場合において、そういった型は、確かにそれらのみえるまま(what they seem to be)の型なのですが、時々そうでないものもあります。ある状況のもとでは、C++はnon-constな型をconstとして扱い、また他の状況ではconstな型をnon-constとして扱うこともあります。

constがいつ現れていつ消えるのか、それを理解することは、C++の内部の挙動についての洞察に役立ち、この言語をより効率的に使用することを可能にするでしょう。

このArticleでは、「変数の有効な型が、その明らかな型と違うのはなぜか、どのようにしてそうなるのか」ということをあなたが理解しやすいような視点で、type declarationとdeductionを、現行の規格のC++と、現在校正中の来るべき新規格(C++0x)の両方で、さまざまな側面から吟味していきます。

もしあなたが、テンプレート引数の推論の基本的なルールに精通していたり、lvalueとrvalueの違いを分かっていたり、lvalueとrvalue参照やauto変数、ラムダ式といった新しいC++0xのfeatureにふれているなら、このArticleは最も有用なものだと気づくでしょう。

いま

int i;

という宣言があったとき、iの型はなんでしょうか?別に引っ掛け問題ではありません、この型はintです。
しかし、iをstructの中においた場合はどうでしょう。(もしくはclassの中に - どちらでも変わらないですが)

struct S{
    int i;
};

S::iの型はなんでしょうか?これもintでしょうか?その通り。ただ、S::iのアドレスを取るとき、あなたはint*ではなく、メンバーポインタ(つまりこの場合はint S::*)を受け取るので、structの中と外のintは完全に同じというわけではありません。しかし、やはりS::iの型はintです。


ではたとえば、私たちがこのstructのポインタを持っている場合、

S *ps;

ps->iの型はなんでしょうか?
(実際、この例ではpsは何を指していなくても関係ありません。たとえクラッシュするような未定義動作を生むようなコードだとしても、式ps->iは一つの型を持っています。)

これもまだ引っ掛け問題ではありません。ps->iの型はintです。
しかし、もし私たちが持っているのがconstへのポインタだった場合は?

const S* pcs;

pcs->iの型はなんでしょうか。なんだかよく分かりませんね。
pcs->iはSの中のint iを指しています。そしてS::iの型はintということについて先ほど考えを一致させました。
しかし、pcsを通してS::iにアクセスするとき、Sはconstなstructになっているので、したがってS::iもconstになるでしょう。
という訳で、pcs->iの型はintではなく、const intになるべきだと考えることが出来ます。


現行のC++(すなわち、1998年に採択された国際標準で定義され、2003年に少しだけ改訂し、今後はC++98と呼ばれるような言語としてのC++)では、pcs->iの型はconst intであり、お話は大体これで終わりです。"新しい"C++(すなわち、今年中にも国際標準に採択されるであろう、通称C++0xと呼ばれる言語)でも、pcs->iの型は同じくconst intであることが、後方互換性のため、保証されています。ただし、この話にはさらにニュアンスが含まれています。

decltype

C++0xからdecltypeが導入されました。これは、compile時に式の型を問い合わせる方法の1つです。
例えば、iの宣言がすでにあるとき、decltype(i)はintになります。
しかし、pcs->iにdecltypeを適用した場合は何が起こるでしょうか?標準化委員会は単にこの型をconst intにしてしまうということも可能でした。
でも、この型をintだとする考えも、合理的でないわけではありません(つまり、S::iの型を、私たちは結局、Sの中のiについて言及しているのだとすれば)。
そして結果的に、変数へのアクセスの式に依存せず、変数を宣言した型を問い合わせることが出来るということは有用だということも分かります。
また、合理的で相反する選択が与えられた時、このC++という言語は、しばしば椅子に深く腰掛けて、あなたに選択をさせようとするでしょう。それはまさに、このケースのように。


あなたが選択したのが、典型的なC++の方法の場合。変数の名前にdecltypeを適用することで、
その変数を宣言した型を取得することができます。例えばこれは、xとyをi、そしてS::iと同じ型で宣言しています。

decltype(i) x;         //x is same type as i, i.e., int
decltype(pcs->i) y;    //y is same type as S::i, i.e., int


それに対して、変数の名前ではなく、式に対してdecltypeを適用した場合は、その式の有効な型(effective type)を取得することが出来ます。
もしその式がlvalueに対応するものなら、decltypeは常にlvalueの参照を返します。すぐ後で例をお見せしますが、その前に私は、
"i"は変数の名前であるのに対して、"(i)"は式であって、変数の名前ではないということについて言及しておかなければなりません。言い換えれば、ある変数の周りにparenthesesを放り投げてやれば、それは、その変数と同じ実行時の値を持つような式になります。
しかし、コンパイル時にはそれはただの式であり、変数の名前ではありません。
これは重要なことです。なぜなら、これの意味するところは、もしあなたがpcsを通してS::iの型を知りたいと思った時、もしpcs->iの周りにparenthesesを付けて問い合わせたなら、decltypeは次の型を返します:

decltype((pcs->i))    // type returnd by decltype is const int& (see also
                      // discussion below)

parenthesesを付けなかった方と比べてみましょう

decltype(pcs->i)     // type returned by decltype is int


私が言及したように、decltypeで、変数の名前ではなく式の型を問い合わせると、その型がlvalueならば、decltypeはlvalueの参照を返します。それが、decltype((pcs->i))がconst int&を返す理由です : pcs->iはlvalueですから。
もしあなたが(constあるいはnon-constでも)intを返す関数を持っているとき、その戻り値の型はrvalueです。そして、decltypeは、その関数呼び出しの戻り値の型をreference-qualifyingしないことで、それを表します。

const int f();      // function returning an rvalue

decltype(f()) x = 0; // type returned by decltype for a call to f is
                     // const int (not const int&), so this declares x
                     // of type const int and initializes it to 0

もしあなたがheavy-dutyなlibrary writerでもない限りは、このような微妙なdecltypeの詳細について心配する必要はないかも知れません。ただ、変数の宣言の型とその変数へアクセスする式の型に違いがあることを知っておくことは確かに価値があることでしょう。そして、それらを区別しようとするとき、decltypeがその助けになるということを知っておくのも、同様に価値があります。

Type Deduction

あなたは、変数の宣言の型と変数へのアクセスの式の型のこの違いは、C++0xからの新しいものと考えたかもしれません。しかし、この違いは本質的にC++98から存在しています。関数テンプレートの型推論の時に何が起こるか考えてみましょう。

template
void f(T p);

int i;
const int ci = 0;
const int *pci = &i;
f(i); // calls f, i.e., T is int
f(ci); // also calls f, i.e., T is int
f(*pci); // call f once again, i.e., T is still int

top-levelのconstはtemplateの型推論の時に取り去られるので*1、型パラメータTはintからもconst intからもintに推論されます。また、これは変数でも式でも同様であり、さらにC++98でもC++0xでも同様です。
つまり、あなたが特定の型の式をtemplateに渡したとき、templateによってみなされる(すなわち推論される)型は、式の型とは違うものになりえます。


C++0xでのauto変数の型推論は本質的に、templateパラメータのそれと同じです。(私の知る限り、唯一の違いは、auto変数がinitializer listから推論されうるのに対して、template parameterの方はそうではないということだけです。)
なので、以下の宣言はどれも(決してconst intではなく)、int型の変数を宣言しています:

auto a1 = i;
auto a2 = ci;
auto a3 = *pci;
auto a4 = pcs->i;

templateパラメータとauto変数の型推論の時に、top-levelのconstのみが除かれます。ポインタか参照を引数に取る関数テンプレートがある時、ポインタの指す先、あるいは、参照が参照している先のconst性はそのまま残ります。

template<typename T>
void f(T &p);

int i;
const int ci = 0;
const int *pci = &i;

f(i);    // as before, calls f<int>, i.e., T is int
f(ci);   // now calls f<const int>, i.e., T is const int
f(*pci); // also calls f<const int>, i.e., T is const int

この振る舞いは旧聞であり、C++98にもC++0xにも適用することが出来ます。auto変数での対応する振る舞いは、もちろんC++0xからですね。

Lambda Expression

C++0xの中で洒落たnew featuresの中にlambda expressionがあります。lambda expressionは関数オブジェクトを生成します。
lambdaから生成されたobjectはclosureとして知られ、生成されたオブジェクトのクラスはclosure typeとして知られています。ローカル変数がlambdaの中にby valueでキャプチャーされたとき、そのlambda用に生成されるclosure typeは、キャプチャーするローカル変数に対応するdata memberを持ち、そのdata memberの型は、キャプチャーする変数の型です。


もしあなたが、C++0xのlambdaに精通していなければ、私はただ回りくどい書き方をしてしまっただけですね。はい。でもちょっと待ってください。明快にするためにベストを尽くしますので。
私があなたに注意して欲しい点は、私が言った、クラスの中のdata memberの型はlocal変数の型と同じということです。それはつまり、"local変数と同じ型"が何を意味しているのかを知る必要があるということです。もしこれが素晴らしくシンプルなら、私はそれについて何も書いたりしないのですが。


でもまず最初にlambdaとclosureとclosure typeについて簡単に説明させてください。私がlambda式の所をハイライトしたこの関数を考えてみましょう。*2

void f(const std::vector<int> &v)
{
    int offset = 4;
    std::for_each(v.cbegin(), v.cend(),
        [=](int i){ std::cout << i + offset; });
}

このlambdaはコンパイラに次のようなクラスを生成させます。

class SomeMagicNameYouNeedNotKnow {
 public:
    SomeMagicNameYouNeedNotKnow(int p_offset): m_offset(p_offset) {}
    void operator()(int i) const { std::cout << i + m_offset; }
 private:
    int m_offset;
};

このクラスがclosure typeです。fの中で、std::for_eachの呼び出しは、まるでこの(closureの)型のobjectがlambda式の代わりに使われているようにみなされます。つまり、std::for_eachの呼び出しは、まるでこのように書いたようなものです。

std::for_each(v.cbegin(), v.cend(), SomeMagicNameYouNeedNotKnow(offset));

ここで私たちにとって興味深いのは、local変数offsetと、closure typeの中でそれに対応する、私がm_offsetと呼んだようなdata memberの関係です。(コンパイラはdata memberに好き勝手な名前をつけることが可能なので、私のm_offsetの使用の中から何かを読み取ったりしないでください。)
与えられたoffsetがint型であるとき、m_offsetの型もint型であるのは特に驚くべきことではありません。
しかし、注意してみてください、lambdaからのclosure type生成のルールにしたがって、closure typeの中のoperator()はconstとして宣言されています。
これはつまり、operator()のbody - lambdaのbodyと同じもの - で、m_offsetはconstだということです。実際的な言い方をすれば、m_offsetの型はconst intであり、そして、もしあなたがそれをlambdaの中から変更しようとすると、あなたのコンパイラは代わりに不機嫌な言葉をくれることでしょう。

std::for_each(v.cbegin(), v.cend(),
              [=](int i) { std::cout << i + offset++; });             // error!

もしあなたが本当に、offsetのコピーを変更したいと思うなら(すなわち、本当にm_offsetを変更したいと思うなら)、
mutable lambdaを使用する必要があります。*3
このlambdaはoperator()関数がconstで無いようなclosure typeを生成します。

std::for_each(v.cbegin(), v.cend(),
              [=](int i) mutable { std::cout << i + offset++; });     // okay

変数をby valueでキャプチャーするとき(すなわち、変数のコピーをキャプチャーするとき)、最初にlambdaにmutableを付けていなければ、コピーしたものの型にconstが付いてしまいます。


関数fの中で、offsetは決して変更されなかったので、あなたはoffsetをconstとして宣言することに決めました。
これは賞賛すべき習慣であり、さらに、期待通り、それはnon-mutableなlambdaには何の影響も及ぼしません。

void f(const std::vector<int>& v)
{
    const int offset = 4;
    std::for_each(v.cbegin(), v.cend(),
                  [=](int i) { std::cout << i + offset; });           // fine
}

ああ、なんということでしょう。mutable lambdaの方では話が違うようです。

void f(const std::vector<int>& v)
{
    const int offset = 4;
    std::for_each(v.cbegin(), v.cend(),
                  [=](int i) mutable { std::cout << i + offset++; }); // error!
}

"えらー?えらー?!,"そんなふうにあなたは言うでしょうか? はい、エラーです。
このコードはコンパイル出来ません。なぜなら、あなたがconst local変数をキャプチャーしたとき、closureの中の変数のコピーもまたconstだからです: ローカル変数のconst性はclosureの中のコピーの型にもコピーされるのです。最後に出てきたlambdaのclosure typeは本質的にはこのようになります:

class SomeMagicNameYouNeedNotKnow {
private:
    const int m_offset;
public:
    SomeMagicNameYouNeedNotKnow(int p_offset): m_offset(p_offset){}
    void operator()(int i) { std::cout << i + m_offset++; }          // error!
};

これは、なぜm_offsetをインクリメントすることが出来なかったかを明らかにしてくれるでしょう。その上、m_offsetがconstとして定義されている(すなわち、point of definitionでconstとして宣言されている)という事実は、あなたがcastを取り払うようなどんなキャストもすることが出来ず、変更可能であるということをあてにすることが出来ないことを意味します。なぜなら、constとして定義されたオブジェクトの値を、castによってconstを取り払って変更しようとした結果は未定義だからです。


なぜ、autoやtemplateパラメータの型推論ではtop-levelのconst性は無視されるのに、closure typeのdata memberの型を推論するときには、もとの型のtop-levelのconstが残っているのか、疑問に思う人もいるでしょう。
あるいはまた、なぜ、この振る舞いをオーバーライドする方法がないのか、疑問に思う人もいるでしょう。すなわち、by-valueでキャプチャーされるローカル変数に対する、closure typeのdata memberが作られるときに、コンパイラに対して、top-levelのconstを無視するように教えたりする方法が。私も、自分で疑問に思いました。しかし、それらに隠された公式な理由は見つけ出すことが出来ませんでした。しかし、もしこのようなコードが有効な場合、

void f(const std::vector<int>& v)
{
    const int offset = 4;
    std::for_each(v.cbegin(), v.cend(),
                  [=](int i) mutable { std::cout << i + offset++; }); // not legal, but
}

実際は操作されているのはclosureの中にあるoffsetのコピーであるのに、a casual readerが、const local変数のoffsetが変更されていると誤って結論づけてしまうということが確認されています。

類似した関係は、なぜnon-mutableなlambdaによって生成されたclosure classのoperator()がconstなのかという理由にもなりえます: lambdaが本当はlocal変数のコピーを変更しているのに、もとのlocal変数を変更していると考えてしまうことから、人々を守るためです:

void f(const std::vector& v)
{
    int offset = 4;                                 // now not const
    std::for_each(v.cbegin(), v.cend(),
        [=](int i) { std::cout << i + offset++; }); // now not mutable, yet
} // the code is still invalid

もし、このコードがコンパイル出来たとして、これはlocal変数のoffsetは変更しません。しかし、誰かがこれをそうでないと思って読んでしまうなんてことは想像に難くありません。


私個人としては、この推理が説得力のあるものかどうかは分かりません。なぜならlambdaの作者がもうはっきりと、結果として生じるclosureがoffsetのcopyを含むように要求してしまっていますし、そして私は、このコードの読者に対して、留意を期待することも出来ると思うからです。
しかし、私が思うに、それは重要なことではありません。重要なことは、lambdaの中で使用される変数がコピーでキャプチャーされるときの型の扱いを、このルールが支配しているということです。

Summary

以下にある、イタリック体の用語は、私が勝手に作ったものです、それらの標準的なものがなかったので。

  • オブジェクトのdeclared typeとは、そのオブジェクトのpoint of declarationの時に持っている型のことです。これは、私たちがその型について普通に考えるままのものです。
  • あるオブジェクトのeffective typeとは、特定の式を通してアクセスするときに持っている型のことです。
  • C++0xにおいて、decltypeは、object declared typeとeffective typeを区別するために使用出来ます。C++98には対応する標準の機能は存在しません。しかし、コンパイラ拡張(例えばgccのtypeof operator)や、Boost.Typeofのようなライブラリを通して、そのほとんどの機能を得ることができます。
  • templateパラメータと、C++0xの auto変数の型推論の間に、top-levelのconstとvolatileは無視されます。
  • lambda式の中でby valueでキャプチャーされるコピーの変数の宣言の型は、オリジナルの変数の型と同じconst性を持っています。non-mutableなlambdaの中では、コピーされた変数の型のeffective typeはつねにconstです。なぜなら、closure classのoperator()はconst member関数だからです。

Acknowledgements

Walter BrightとBatosz MilewskiはこのArticleの原稿に有益なコメントを提供してくれました。そして、Eric Nieblerは寛大にも、このArticleをC++Nextで発表する準備に賛成してくれました。

Displaying Types

残念ながら、C++には変数や式の型を表示する標準的な方法はありません。私たちが持っている最も類似した方法は、typeidから取得できる、std::type_infoクラスのnameメンバ関数です。これは時々かなり良く働くのですが、

int i;
std::cout << typeid(i).name(); // produces "int" under Visual C++,
                               // "i" under gcc.

時々は、そうではありません。

const int ci = 0;
std::cout << typeid(ci).name(); // same output as above, i.e.,
                                // const is omitted

あなたはこれを、テンプレートの技術を使って型を分解し、型の構成要素それぞれの文字列表現を生成して、それらを表示用の形式に再構成することで、より良くすることが出来るでしょう。
もしあなたの運が良ければ、私がUsenetのcomp.lang.c++ニュースグループでこれについて質問した時のように、すでにこれを作り上げた人を見つけることが出来るかもしれません。
Marc ClisseはPrinttype templateを提示してくれました。これは、以下のコードをgccコンパイルしたときに、あなたが期待する出力のほとんどを生成します。(この例はMarcのコードから引用しました。)

struct Person{};
std::cout << Printtype<unsigned long*const*volatile*&()>().name();
std::cout << Printtype<void(*&)(int(void),Person&)>().name();
std::cout << Printtype<const Person&(...)>().name();
std::cout << Printtype<long double(volatile int*const,...)>().name();
std::cout << Printtype<int (Person::**)(double)>().name();
std::cout << Printtype<const volatile short Person::*>().name();

MarcのアプローチはC++0xの機能であるvariadic templateを使用しています。variadic templateは現在、私の知る限り、gccでしかサポートされてませんが、彼のコードは少々の変更で、C++98準拠の多くのコンパイラでも同じように働くだろうと私は考えています。
MarcのPrinttype templateは、以下のreferenceから取得可能です。

Further Information

  • C++0x entry at Wikipedia. A nice, readable, seemingly up-to-date overview of language and library features introduced by C++0x.
  • Working Draft, Standard for Programming Language C++, ISO/IEC JTC1/SC22/WG21 Standardization Committee, Document N3225, 27 November 2010. This is the nearly-final C++0x draft standard. It’s not an easy read, but you’ll find definitive descriptions of decltype in section 7.1.6.2 (paragraph 4), of auto in section 7.1.6.4, and of lambda expressions in section 5.1.2.
  • decltype entry at Wikipedia. A nice, readable, seemingly up-to-date overview of C++0x’s decltype feature.
  • Decltype (revision 5), Jaako Järki et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee Document N1978, 24 April 2006. Unlike the draft C++ standard, this document provides background and motivation for decltype, but the specification for decltype was modified after this document was written, so the specification for decltype in the draft standard does not exactly match what this document describes.
  • Referring to a Type with typeof, GCC Manual. Describes the typeof extension.
  • Boost.Typeof, Arkadiy Vertleyb and Peder Holt. Describes the Boost.Typeof library.
  • Deducing the type of variable from its initializer expression, Jaako Järvi et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee Document N1984, 6 April 2006. This document gives background and motivation for auto variables. The specification it proposes may not match that in the draft standard in every respect. (I have not compared them, but it’s common for details to be tweaked during standardization.
  • Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4), Jaako Järvi et al, ISO/IEC JTC1/SC22/WG21 Standardization Committee Document N2550, 29 February 2008. This is a very brief summary of lambda expressions, but it references earlier standardization documents that provide more detailed background information. The specification for lambdas was revised several times during standardization.
  • Overview of the New C++ (C++0x), Scott Meyers, Artima Publishing, 16 August 2010 (most recent revision). Presentation materials from my professional training course in published form. Not as formal or comprehensive as the standardization documents above, but much easier to understand.
  • String representation of a type, Usenet newsgroup comp.lang.c++, 28 January 2011 (initial post). Discussion of how to produce displayable representations of types. Includes link to Marc Glisse’s solution.

次回は

わてすさんのゼロから作る! template で構文解析メタプログラム - メモ代わりです。
よろしくお願いします。

*1:top-levelのvolatileもtop-levelのconstと同じように扱われます。しかし、"top-levelのconstあるいはvolatile"と繰り返しいうのを避け、コメントをconstのみに制限しました。そして、C++はvolatileも同様に扱うというあなたの理解をあてにしています

*2:訳注:原文では下のコードスニペットラムダ式がハイライトされている

*3:そうするかもしくは、変更したい時に毎回offsetのコピーをcastしてconstを取り払ってもいいでしょう。mutable lambdaを使うほうがより理解しやすいと思います。