定数式と識別子 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、定数式を取り上げます。定数式はこれまでにも登場しているのですが、プログラムの分かりやすさの面では少し問題もあります。この問題を解消するために必要な、識別子についても取り上げます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



定数式

これまでのページで登場した計算式では、3 + 5 とか 10 / 2 といったように、数を整数リテラル(「計算」のページを参照)として、ソースコードに直接書き込んできました。

じつは、リテラルだけを使って書かれている計算式は、コンパイルのときに計算を行っています。プログラムを実行したあと、実行中に計算していると思ったかもしれませんがそうではありません。

このように、コンパイルの最中(コンパイル時 (compile time))に計算できる式のことを、定数式 (constant expression)といいます。

【C++98/03 経験者】定数式の考え方は注目すべきです。C++11 以降、コンパイル時に済ませられることは極力、コンパイル時に終わらせようという方向性での機能追加・改善が進められています。分岐やループ、関数呼び出しを含むような処理であってもコンパイル時に行えます。

定数式になっている計算式は、プログラムの実行ファイルが作られたときにはもう計算済みですから、プログラムを実行をしてから行う仕事は残っていません。

std::cout を使って、結果を出力する仕事は、実行してから行う仕事です。std::cout はリテラルではないので、std::cout << の部分は定数式ではありません。

定数式を使うと、実行してから行う仕事が減るので、より速いプログラムを作る助けになります。

しかし、定数式から生み出される結果はいつもまったく同じであり、何の柔軟性もありません。3 + 5 からは 8 しか生まれません。「3 + 5」しかできない電卓に価値がないのと同じで、定数式だけでは価値あるプログラムを作ることができません。

「自由に計算式を入力して、その結果を出力できるプログラム」ができれば素晴らしいですが、それにはまだ知識が足りません。それでも悪くない目標なので、この先のページで少しずつ順を追って知識を増やしていくことにします。このページではまず、定数式でできる範囲で、プログラムをより良くすることを考えてみます。

リテラルの問題

定数式にはリテラルと演算子だけが並びますが、これを見せられただけでは、その意味を読み取ることは困難です。たとえば、(10 - 1) * 1000 という計算式があるとして、10 にはどんな意味があるのかは、この式からは読み取れません。11000 も同様に意味がみえません。

「この記述はどういう意味なのか?」が読み取れない(あるいは読み取りにくい)ソースコードは、「可読性 (readability) が悪い」といって、改善の余地があるプログラムです。また、意味がみえないリテラルのことを、マジックナンバー (magic number) と呼ぶことがあります。

この分かりづらさは、リテラルが、人間に対してこれといった情報を与えてくれないところに原因があります。たとえば、10 が「応募者の人数」、1 が「欠席者の人数」、1000 が「参加費」だという情報があれば、(10 - 1) * 1000 は、徴収できた参加費の合計を計算しているとわかります。

つまり、ソースコードに (会員の人数 - 欠席者の人数) * 参加費 のように書ければ明確になります。しかし、もちろんこれではコンパイルは失敗してしまいます。

#include <iostream>

int main()
{
    std::cout << (会員の人数 - 欠席者の人数) * 参加費 << "\n";
}

このようなソースコードが書けたとしても、今度は「会員の人数」「欠席者の人数」「参加費」がいくつなのかが分からなくなっています。1011000 といった数はソースコードに残っていないといけません。

そこで、整数リテラルとその意味を結びつけるために、constexpr変数 (constexpr variable) を使います。

constexpr変数

constexpr変数という機能を使って、リテラルとその意味に結びつきを与えます。

まずは、プログラムを見てください。

#include <iostream>

int main()
{
    constexpr auto number_of_members = 10;
    constexpr auto number_of_absentees = 1;
    constexpr auto entry_fee = 1000;

    std::cout << (number_of_members - number_of_absentees) * entry_fee << "\n";
}

実行結果:

9000

これまでのプログラムでは、main関数の内側には std::cout を使った出力の処理しかありませんでした。今回は、そうではない記述が追加されています。

constexpr auto number_of_members = 10; といった記述によって、リテラルに名前を付けています。記法はこうです。

constexpr auto 名前 = リテラル;

ここではリテラルに名前を付けることを目的にしているので、= の右側を「リテラル」としていますが、「定数式」を書くことができます。そのため、constexpr auto total_fee = (number_of_members - number_of_absentees) * entry_fee; のように書いてしまうこともできます。

【C言語プログラマー】auto というキーワードはC言語にもありますが、C++ では意味が異なります(下のコラム参照)

【上級】auto は型の指定です。「初期化子の内容をみて、コンパイラが自動的に型を判断せよ」という意味になります。たとえば、初期化子が 10 であれば、int型であると判断されます。constexpr int number_of_members = 10; のように書けないわけではありません。

constexpr auto number_of_members = 10; の場合なら、10 という整数リテラルに、number_of_members という名前を付けています。この記述をしておくと、ソースコードの後続の部分で、10 の代わりに number_of_members という名前を使えるようになります。

_ という記号を使っているのは、単語の区切りを明確にするためです。あとで取り上げますが、名前の一部として使える記号は _ しかありません。

constexpr auto number_of_absentees = 1;constexpr auto entry_fee = 1000; も同様です。1 の代わりに number_of_absentees が使えるようになり、1000 の代わりに entry_fee が使えるようになります。

これらの記述によって、計算式の部分では (number_of_members - number_of_absentees) * entry_fee と書けるようになりました。この計算式は (10 - 1) * 1000 と書いているのと何ら変わりません。名前で記述するようになりましたが、変わらず定数式であり、計算はコンパイルのときに行われています。

リテラルに名前を付けたことで、式の意味が明確になりました。あとから数だけを変えたいと思ったとき、どこを修正すればいいのか分かりやすくなりました。たとえば、会員が増えて 11人になった場合には、constexpr auto number_of_members = 10;constexpr auto number_of_members = 11; に変更すればいいです。

【C++98/03 経験者】C++98/03 では、このような目的で const や enum、#define を使っていましたが、現在の C++ では、使えるときには constexpr を使うようにしましょう。const はコンパイル時の計算を保証しません。enum では整数リテラルにしか対応できませんし、#define はマクロ置換に伴う危険性や、デバッガからは名前がみえない場合があるなどの問題があります。

【C言語プログラマー】C言語では、このような目的で enum や #define を使っていましたが、C++ では、使えるときには constexpr を使うようにしましょう。enum では整数リテラルにしか対応できませんし、#define はマクロ置換に伴う危険性や、デバッガからは名前がみえない場合があるなどの問題があります。

文字列リテラルの場合

文字列リテラルの場合でも同様のことができます。

#include <iostream>

int main()
{
    constexpr auto greeting = "Hello.";

    std::cout << greeting << "\n";
}

実行結果:

Hello.

"\n" も greeting に含めてしまう手もありますが、そうすると、改行したくない場面で使うことができなくなります。

識別子

ここまで「名前」と表現しましたが、正確な用語を使うと、識別子 (identifier) といいます。

識別子とは、複数のものがあるとき、それぞれを区別するための情報のことです。C++ においては、それはつまり「名前」です。

識別子として使える文字は、次のように決められています。1

  1. アルファベット (a~z、A~Z)
  2. 数字 (0~9)
  3. アンダースコア (_)
  4. ユニバーサル文字名
  5. 処理系定義の文字

4のユニバーサル文字名は難しいので、ここでは取り上げません(コラム参照)。

【上級】ユニバーサル文字名とは、ISO/IEC 10646 で定められた文字コードを使い、\u3042 のような形式で文字を記述する方法です(これは「あ」を意味しています)2

5の処理系定義の文字とは、各コンパイラがそれぞれの判断で使えるようにしている文字ということです。

処理系というのは、ソースコードを解釈して実行するために必要な環境のことです。コンパイラも処理系の一部です。

処理系定義というのは、処理系が仕様を決めるという意味の C++ の用語です。

識別子に日本語を使いたいと思うかもしれません。それが可能かどうかは、5の処理系定義の文字に、日本語の文字が含まれているかどうか次第ということになります。つまり、コンパイラ次第だということです。新C++編としては、1~3に含まれる文字だけを使う方針で進めます。


一方で、使うことができない識別子のルールもあります。使うことができない識別子を使おうとすると、コンパイルエラーになります。

  1. 先頭に数字は使えない
  2. キーワードは使えない
  3. 代替表現は使えない
  4. 予約語は使えない

キーワード (keywords) とは、C++ の機能を表現するために使われている名前のことです。覚える必要はありませんが、もしかしたら知らずに同じ名前を使おうとして、コンパイルエラーが起こることがあるかもしれませんから、C++14 に存在するキーワードのリスト3を挙げておきます。

alignas    continue     friend    register         true
alignof    decltype     goto      reinterpret_cast try
asm        default      if        return           typedef
auto       delete       inline    short            typeid
bool       do           int       signed           typename
break      double       long      sizeof           union
case       dynamic_cast mutable   static           unsigned
catch      else         namespace static_assert    using
char       enum         new       static_cast      virtual
char16_t   explicit     noexcept  struct           void
char32_t   export       nullptr   switch           volatile
class      extern       operator  template         wchar_t
const      false        private   this             while
constexpr  float        protected thread_local
const_cast for          public    throw

代替表現 (alternative representations) はあまり使われることはないですが、演算子の記号の代わりに使える表現のことで、以下のものがあります。4

and     compl   or_eq
and_eq  not     xor
bitand  not_eq  xor_eq
bitor   or  

予約語 (reserved word) とは、C++ の現在や将来の実装で使うために確保されている名前のことです。特定の名前が定められているのではなく、以下の条件に当たるものが予約語です。5

  1. 2文字以上の連続する _ が含まれた名前
  2. 先頭が _ で、2文字目が大文字のアルファベットになっている名前


なお、区別が付かなければならないので、同じ識別子を、別のものをあらわすために使うことはできません。

#include <iostream>

int main()
{
    constexpr auto greeting = "Hello.";
    constexpr auto greeting = "Good, Morning.";

    std::cout << greeting << "\n";
}

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のうち、識別子として使えない名前はどれか選んでください。

  1. __abc
  2. ab__c
  3. abc_
  4. _abc
  5. _ABC
  6. int
  7. INT
  8. a10
  9. 10a
  10. _

解答・解説

問題2 (確認★)

std::cout を使って出力する式、たとえば、std::cout << 3 は定数式ではありません。それはなぜですか。

解答・解説

問題3 (基本★★)

長方形の面積を求めるプログラムを、constexpr変数を使って作成してください。辺の長さや単位は自由にして構いません。

解答・解説

問題4 (基本★★)

ある同じ重さの物体がいくつか存在するとき、全体の重さを求めるプログラムを、constexpr変数を使って作成してください。重さや単位は自由にして構いません。

解答・解説

問題5 (調査★★★)

識別子の付け方には色々な考え方・ルールがあり、「命名規則」と呼ばれています。命名規則について調べてみてください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る