「クラスのメンバ関数の中から、同じ型のグローバル配列にアクセスしたい」という相談は、C++のコードレビューでとてもよく見かけます。一見シンプルな要望ですが、そのまま実装するとコンパイルエラーになったり、テストしづらい“扱いにくいコード”になりがちです。本記事では、典型的な失敗例から、モダンC++での安全な書き方まで、実務を意識して整理します。
C++で「クラス内からグローバル配列を使いたい」状況
まずは、よくあるイメージのコードを簡略化してみます。
class Path {
public:
int id;
bool r;
float get_x() const {
// ここでグローバル配列 Paths を走査したい
for (int i = 0; i < 30; ++i) {
if (Paths[i].r) {
return static_cast<float>(Paths[i].id);
}
}
return 0.0f;
}
};
Path Paths[30]; // グローバル配列
このように書くと、たいてい次のようなエラーになります。
Pathsが未宣言だと言われる- ヘッダとソースに分けたときに多重定義になる
なぜかというと、クラスのメンバ関数をその場で定義したタイミングでは、まだ Paths が見えていないからです。また、設計的にも「クラスから特定のグローバル配列に直結している」状態は結合が強く、再利用性やテスト容易性を大きく損ねます。
そこで、本記事では次の方針で解決します。
- ベストプラクティス:配列(またはコンテナ)を引数で受け取る
- やむを得ない場合の最小修正:
extern宣言+クラス外定義でグローバル配列を参照
なぜグローバル配列への直接依存が問題になるのか
グローバル配列をクラスの中から直接触ろうとすると、次のような問題を抱え込みます。
| 問題の種別 | 具体的な内容 |
|---|---|
| コンパイル順序の問題 | メンバ関数定義時点では Paths が見えておらず、未宣言識別子エラーになる。 |
| 結合の強さ | Path クラスが「特定のグローバル配列」に依存し、他の配列で再利用しづらい。 |
| テストのしづらさ | テストごとにグローバル配列を差し替える必要があり、並列実行やモック化が難しい。 |
| 設計の拡張性 | 将来コンテナを std::vector や std::array に変えたいときに影響範囲が大きくなる。 |
これらをまとめると、「動けばよい」という観点ならグローバルに直結しても構いませんが、長く保守するコードでは確実に足を引っ張ります。そのため、本記事ではまずグローバルに直接依存しない設計から紹介します。
推奨パターン1:配列・コンテナを引数で受け取る
最もシンプルで安全なのは、「クラスのメンバ関数(または静的メンバ関数)に対象の配列を渡す」という設計です。グローバル配列を使いたければ、呼び出し側でそれを渡すだけです。
C++20以降:std::spanで一括対応する
C++20から導入された std::span は、「連続したメモリを指す軽量なビュー」です。std::array、std::vector、C配列(Path[30])を一つの関数で同じように扱えるので、今回の用途に最適です。
#include <array>
#include <vector>
#include <span>
#include <optional>
class Path {
public:
int id{};
bool r{}; // true なら有効とする
// 有効な要素のうち、最初の id を返す
static std::optional<int> first_id(std::span<const Path> paths) {
for (const auto& p : paths) {
if (p.r) {
return p.id;
}
}
return std::nullopt; // 見つからなかった
}
};
int main() {
// C配列
Path paths_c[30]{};
// std::array
std::array<Path, 30> paths_array{};
// std::vector
std::vector<Path> paths_vec(30);
// どれでも同じAPIで呼べる
auto id1 = Path::first_id(paths_c);
auto id2 = Path::first_id(paths_array);
auto id3 = Path::first_id(paths_vec);
// 戻り値の扱い例
if (id1) {
// 見つかった
int value = *id1;
(void)value;
}
}
std::span を使うことで、次のようなメリットがあります。
- 配列長を別引数で渡す必要がない
- C配列・
std::array・std::vectorを共通のインターフェースで処理できる - ポインタよりも意図が明確で、安全性が高い
よくあるポインタ版と比較すると、意図がずっと読み取りやすくなります。
| 書き方 | 特徴 |
|---|---|
Path* p, std::size_t n | どこまでが有効範囲か分かりにくい。配列長を間違えて渡してもコンパイル時に検出できない。 |
std::span<const Path> | 「要素群」を表す型。長さも内部に含まれるため、安全かつ表現力が高い。 |
C++17以前:テンプレートでC配列参照を受け取る
C++17以前でも、テンプレートを使えば「配列長を型として受け取る」ことができます。コンパイラが配列長を推論してくれるので、呼び出し側はシンプルなままです。
#include <cstddef>
class Path {
public:
int id{};
bool r{};
template <std::size_t N>
static int first_id(const Path (&paths)[N]) {
for (std::size_t i = 0; i < N; ++i) {
if (paths[i].r) {
return paths[i].id;
}
}
return -1; // 見つからない場合の仕様は要件に合わせて決める
}
};
int main() {
Path Paths[30]{};
int id = Path::first_id(Paths); // N = 30 が自動で推論される
}
このテンプレート版の特徴は次のとおりです。
- 配列長
Nがテンプレート引数として伝わるため、境界外アクセスをしにくい - 関数側で配列長を変更しても、呼び出し側のコードは変えずに済む
- C++17以前でも利用可能
ただし、この方法は「C配列に限定」されるため、std::vector や std::array を扱う場合は別のオーバーロードを用意するか、引数を std::vector<Path>& などに変える必要があります。
標準コンテナ(std::array / std::vector)で管理する
そもそも C配列をやめて、最初から標準コンテナで管理するのも有力な選択肢です。固定長なら std::array、可変長なら std::vector を使いましょう。
#include <array>
#include <optional>
class Path {
public:
int id{};
bool r{};
};
std::optional<int> first_id(const std::array<Path, 30>& paths) {
for (const auto& p : paths) {
if (p.r) {
return p.id;
}
}
return std::nullopt;
}
int main() {
std::array<Path, 30> paths{};
// 初期化など ...
auto id = first_id(paths);
}
標準コンテナを使うことで、次のような利点があります。
- 範囲for文が使いやすい
.size()や.at()が使えるため、安全に要素数や範囲チェックが行える- メモリの初期化やコピーがしやすい
| コンテナ | 向いている場面 |
|---|---|
Path[30](C配列) | 既存の古いコードを最小変更で触る場合。新規コードでは非推奨。 |
std::array<Path, 30> | 固定長で、要素数がコンパイル時に決まる場合。 |
std::vector<Path> | 要素数が可変、あるいは動的に増減する場合。 |
推奨パターン2:コンテナ管理クラスを用意する
「Path 自身が配列を意識しない」ように、配列側を管理するクラスを用意する設計もよく使われます。責務を分けることができ、クラス同士の結合を弱められます。
#include <array>
#include <optional>
class Path {
public:
int id{};
bool r{};
};
class PathManager {
public:
std::array<Path, 30> paths{};
std::optional<int> first_id() const {
for (const auto& p : paths) {
if (p.r) {
return p.id;
}
}
return std::nullopt;
}
};
このようにすると、
Pathは「1つの経路」を表すだけのシンプルなクラスになる- 複数の配列を扱いたい場合も
PathManagerのインスタンスを複数持てばよい - テスト時に、テスト専用の
PathManagerを簡単に用意できる
結果として、実装も保守も見通しが良くなります。
最小修正でグローバル配列を使う方法(非推奨)
「設計を大きく変えられないが、とりあえずコンパイルを通したい」という状況もあると思います。その場合は、extern でグローバル配列を前方宣言し、メンバ関数定義をクラス外に出すという手があります。
ヘッダファイル側の例
// Path.h
#pragma once
#include <cstddef>
class Path {
public:
int id{};
bool r{};
float get_x() const; // ここでは宣言だけ
};
extern Path Paths[30]; // グローバル配列の宣言(定義ではない)
ソースファイル側の例
// Path.cpp
#include "Path.h"
float Path::get_x() const {
for (std::size_t i = 0; i < 30; ++i) {
if (Paths[i].r) {
return static_cast<float>(Paths[i].id);
}
}
return 0.0f;
}
// 実体定義(ここは1つの翻訳単位だけ)
Path Paths[30];
この形なら、
Pathsはヘッダで宣言されているため、クラス外定義のPath::get_x()から参照できる- 実体は
Path.cppのみで定義されるため、多重定義を防げる
ただし、次のような欠点が残ります。
Pathクラスが特定のグローバル配列Pathsに強く依存したまま- テスト毎に
Pathsの中身を変える必要があり、テストコードと本番コードが密結合 - マルチスレッド環境では、グローバルを書き換える処理にロックなどが必要になる
そのため、このやり方は「既存コードの延命」と割り切り、余裕ができたタイミングで引数注入(上で紹介した span / テンプレート案)に段階的に移行するのがおすすめです。
よくあるエラー・バグの芽と対策
実装時につまずきやすいポイントを具体例とともにまとめます。
未宣言識別子エラー(’Paths’ was not declared in this scope)
典型的なパターンは次のようなコードです。
class Path {
public:
int id;
bool r;
float get_x() const {
return static_cast<float>(Paths[0].id); // ← ここでエラー
}
};
Path Paths[30];
クラス定義の時点では Paths はまだ宣言されていないため、コンパイラは Paths を認識できません。対策としては、
- 本記事の推奨通り「配列を引数で受け取る」設計に変える
- どうしてもグローバルを使うなら
extern宣言+クラス外定義にする
といった方法があります。
メンバ未定義によるコンパイルエラー
回答やサンプルコードを写経した時に、Path に必要なメンバを定義し忘れているケースもよくあります。
class Path {
public:
// int id; // ← これを忘れると Paths[i].id がコンパイルできない
bool r;
};
Paths[i].id や Paths[i].r を使う場合は、クラス側で対応するメンバを必ず定義してください。
戻り値が未定義になる(return の抜け)
条件にマッチした要素が見つからなかった場合の戻り値を決めていないと、未定義動作になります。
int Path::first_id(std::span<const Path> paths) {
for (const auto& p : paths) {
if (p.r) {
return p.id;
}
}
// ここで何も返さないと未定義動作
}
対策としては次のような選択肢があります。
std::optional<int>を返して、「見つからない」こと自体を表現する- 見つからない場合は
-1や0などの特別な値を返す(仕様を明文化すること)
モダンC++では、意味の分からない魔法値よりも std::optional を使う方が読みやすく、安全です。
戻り値型と実際の値の型がちぐはぐ
質問コードでよく見かけるのが、float get_x() で実際には id(int)を返しているパターンです。コンパイルは通りますが、意図が読み取りづらく、バグの温床になります。
float Path::get_x() const {
// 実際は座標でなく id を返している
return static_cast<float>(id);
}
この場合は、
- 本当に座標の
xを返したいなら、その情報をメンバとして持たせる idを返したいなら、関数名をget_id()やfirst_id()に変える
といった形で、関数名と戻り値の意味を合わせることが重要です。
非標準マクロ(ARRAYSIZEなど)に依存している
Windows系のコードサンプルでは、配列長を求めるために ARRAYSIZE マクロを使っていることがあります。しかしこれは標準C++の機能ではないため、移植性に欠けます。
代わりに次のように書きましょう。
- C++17以降:
std::size(Paths)(<iterator>が必要) - それ以前:
sizeof(Paths) / sizeof(Paths[0])もしくはテンプレートで配列長を受け取る
#include <iterator>
Path Paths[30];
for (std::size_t i = 0; i < std::size(Paths); ++i) {
// ...
}
型名と変数名が紛らわしい
Path と Paths のように、型名と変数名が似すぎていると、読み手が混乱しやすくなります。例えば次のようなコードです。
Path Paths[30];
Path* path = &Paths[0];
規模が大きいコードでは、Path が型なのか、変数なのか、一瞬では判別しづらくなります。次のように名前を工夫すると読みやすくなります。
- 型名:
Path - 配列変数名:
g_paths、path_table、path_listなど
特にグローバル変数であることを示すために、g_ プレフィックスなどを採用するプロジェクトも多いです(スタイルガイドに合わせてください)。
グローバル依存を減らすとテストが楽になる
ここまでの話を「テスト」の観点からまとめると、次のようになります。
| パターン | テストのしやすさ |
|---|---|
| クラスがグローバル配列に直接依存 | テストごとにグローバルを書き換える必要がある。並列テストがほぼ不可能。 |
| 配列/コンテナを引数で受け取る | テスト専用の配列/コンテナを渡すだけでよい。モック化やデータ差し替えが容易。 |
| 管理クラス(PathManagerなど)を挟む | 管理クラス自体を差し替えたり、テスト専用実装を用意できる。 |
モダンC++の設計では、関数やクラスに「必要なものを引数で渡す(依存性注入)」という考え方が基本になっています。グローバル変数にベタッと依存していると、あとからの変更やテストのたびに苦労するため、新規コードでは極力避けることをおすすめします。
既存コードを段階的にリファクタリングする例
最後に、「今あるコードをどう直していけばよいか」を具体例で見てみます。
Before:クラス内で直接グローバル配列を参照
class Path {
public:
int id;
bool r;
int first_id() const {
for (int i = 0; i < 30; ++i) {
if (Paths[i].r) {
return Paths[i].id;
}
}
return -1;
}
};
Path Paths[30];
この状態では、first_id() が特定のグローバル配列 Paths に依存しており、別の配列では再利用できません。
Step1:メンバ関数を静的メンバにして、配列を引数に取る
class Path {
public:
int id;
bool r;
static int first_id(const Path* paths, std::size_t n) {
for (std::size_t i = 0; i < n; ++i) {
if (paths[i].r) {
return paths[i].id;
}
}
return -1;
}
};
呼び出し側は次のように変わります。
Path Paths[30];
int id = Path::first_id(Paths, 30);
この時点で、グローバル配列への直接依存は解消されています。
Step2:C++20なら std::span に差し替える
class Path {
public:
int id{};
bool r{};
static std::optional<int> first_id(std::span<const Path> paths) {
for (const auto& p : paths) {
if (p.r) {
return p.id;
}
}
return std::nullopt;
}
};
呼び出し側はそのままでも動きますし、将来的に std::array や std::vector に置き換えても、
std::array<Path, 30> paths{};
auto id = Path::first_id(paths);
といった形で、呼び出しコードの変更を最小限に抑えられます。
まとめ:グローバル配列に直接触らず、引数注入で設計する
本記事で扱ったポイントを最後に整理します。
- クラス内から特定のグローバル配列を直接参照する設計は、結合が強くテストしづらい。
- モダンC++では、配列やコンテナを関数の引数として渡すのがベストプラクティス。
- C++20以降なら
std::spanを使うと、C配列・std::array・std::vectorを一括して扱える。 - C++17以前なら、テンプレートで C配列参照を受けるか、
std::array/std::vectorを使う。 - どうしてもグローバル配列を使いたい場合は、
extern宣言+クラス外定義でコンパイルは通るが、設計上は非推奨。 - 未宣言識別子、戻り値の未定義、型のちぐはぐなどの典型的なバグの芽を、実装段階で確実に潰す。
一度「配列を引数で受け取る」スタイルに慣れてしまえば、グローバル配列に直接触りたい場面はほとんど無くなります。これから既存コードを改善していく際は、std::span や標準コンテナを活用しつつ、グローバル依存を少しずつ減らしていくことを意識してみてください。

コメント