ядро языкаcore language

многомерный operator[]multidimensional operator[]

operator[] теперь может принимать несколько аргументов через запятую: matrix[i, j] вместо matrix(i, j) или цепочки matrix[i][j] с промежуточным proxy-объектом.operator[] can now take multiple comma-separated arguments: matrix[i, j] instead of matrix(i, j) or a chain of matrix[i][j] with an intermediate proxy object.

осторожно: молчаливая смена смысла старого кодаcaution: silently changes the meaning of old code частый вопрос: связь со std::mdspancommon question: the link to std::mdspan

Что этоWhat it is

До C++23 operator[] был строго ограничен ровно одним параметром — это ограничение унаследовано с самых ранних версий языка. C++23 снимает это ограничение: теперь можно объявить operator[] с произвольным числом параметров, и вызов matrix[i, j] напрямую разрешается в такую перегрузку, без запятой-оператора и без промежуточных объектов.Before C++23, operator[] was strictly limited to exactly one parameter — a restriction inherited from the language's earliest versions. C++23 lifts that restriction: you can now declare operator[] with any number of parameters, and a call like matrix[i, j] resolves directly to such an overload, with no comma operator and no intermediate objects involved.

до C++23 — operator() или proxy-цепочка [][]before C++23 — operator() or a [][] proxy chain
class Matrix {
public:
    double& operator()(std::size_t i, std::size_t j) {  // круглые скобки вместо привычных []// parens instead of the familiar []
        return data_[i * cols_ + j];
    }
private:
    std::vector<double> data_;
    std::size_t cols_;
};
m(2, 3) = 1.5;  // менее привычный синтаксис для индексации// a less familiar syntax for indexing
C++23 — многоаргументный operator[]C++23 — multi-argument operator[]
class Matrix {
public:
    double& operator[](std::size_t i, std::size_t j) {  // квадратные скобки, несколько параметров// square brackets, multiple parameters
        return data_[i * cols_ + j];
    }
private:
    std::vector<double> data_;
    std::size_t cols_;
};
m[2, 3] = 1.5;  // естественный синтаксис индексации, без proxy// natural indexing syntax, no proxy

Зачем ввелиWhy it was introduced

Ограничение operator[] одним параметром было чисто исторической случайностью, а не осознанным дизайн-решением — оно годами мешало естественному выражению многомерного доступа:The single-parameter restriction on operator[] was purely a historical accident rather than a deliberate design choice — for years it stood in the way of naturally expressing multidimensional access:

01

operator() — менее узнаваемый синтаксис для индексацииoperator() is a less recognizable syntax for indexing

Авторы матричных и тензорных библиотек, которым нужен был многомерный доступ, были вынуждены перегружать operator() вместо operator[], потому что только он допускал произвольное число аргументов. Но круглые скобки в первую очередь ассоциируются с вызовом функции, а не с индексацией — синтаксис m(i, j) читается менее очевидно, чем m[i, j], для программиста, привыкшего к квадратным скобкам как к маркеру "это контейнер, а не функция".Authors of matrix and tensor libraries who needed multidimensional access were forced to overload operator() instead of operator[], since only it allowed an arbitrary number of arguments. But parentheses are primarily associated with a function call, not indexing — the syntax m(i, j) reads less obviously than m[i, j] to a programmer used to square brackets as the marker for "this is a container, not a function".

02

Proxy-цепочка [][] — лишний слой косвенностиA [][] proxy chain is an extra layer of indirection

Альтернатива — matrix[i][j] — требовала, чтобы первый operator[] возвращал вспомогательный proxy-объект (например, "строку" матрицы), у которого затем вызывается второй operator[]. Это добавляло дополнительный тип в публичный API исключительно ради синтаксиса, усложняло инлайнинг и оптимизацию (компилятору сложнее видеть сквозь два вложенных вызова с промежуточным временным объектом, чем через одно прямое обращение), и плохо масштабировалось на произвольную размерность.The alternative — matrix[i][j] — required the first operator[] to return a helper proxy object (e.g. a matrix "row"), on which a second operator[] is then called. This added an extra type to the public API purely for the sake of syntax, complicated inlining and optimization (it's harder for the compiler to see through two nested calls with an intermediate temporary than through one direct access), and scaled poorly to arbitrary dimensionality.

03

std::mdspan нуждался в естественном синтаксисе для N измеренийstd::mdspan needed a natural syntax for N dimensions

C++23 одновременно вводит std::mdspan — многомерное невладеющее представление данных произвольной размерности. Без многоаргументного operator[] у mdspan не было бы единообразного, естественно читаемого способа индексации md[i, j, k] для тензора любой размерности — пришлось бы городить либо operator(), ломающий консистентность с остальной STL, либо proxy-цепочки, не масштабирующиеся за пределы 2-3 измерений.C++23 simultaneously introduces std::mdspan — a multidimensional non-owning data view of arbitrary dimensionality. Without a multi-argument operator[], mdspan would have had no uniform, naturally readable way to index md[i, j, k] for a tensor of any dimensionality — it would have had to resort to either operator(), breaking consistency with the rest of the STL, or proxy chains that don't scale past 2-3 dimensions.

Как это работает под капотомHow it works under the hood

Синтаксически m[i, j] — это не список из двух выражений, разделённых оператором запятая, а специальный список аргументов для operator[], аналогичный списку аргументов при вызове функции. Компилятор напрямую разрешает его в перегрузку operator[](T1, T2, ...) с соответствующим числом параметров, каждый аргумент передаётся отдельно — как в обычном вызове функции с несколькими параметрами.Syntactically, m[i, j] isn't a list of two expressions separated by the comma operator, but a special argument list for operator[], analogous to a function call's argument list. The compiler resolves it directly to an operator[](T1, T2, ...) overload with the matching number of parameters, each argument passed separately — just like an ordinary function call with several parameters.

template<class T>
class Matrix {
public:
    Matrix(std::size_t rows, std::size_t cols)
        : data_(rows * cols), rows_(rows), cols_(cols) {}

    T& operator[](std::size_t i, std::size_t j) {
        return data_[i * cols_ + j];  // i и j пришли как два отдельных параметра// i and j arrive as two separate parameters
    }
    const T& operator[](std::size_t i, std::size_t j) const {
        return data_[i * cols_ + j];
    }
private:
    std::vector<T> data_;
    std::size_t rows_, cols_;
};

Matrix<double> m(3, 3);
m[0, 0] = 1.0;  // напрямую вызывает operator[](0, 0)// directly calls operator[](0, 0)
m[1, 2] = 3.5;

Ровно этот механизм использует std::mdspan: доступ md[i, j, k] по сути вызывает operator[], принимающий столько индексов, сколько измерений (extents) описывает mdspan, и внутренне транслирует их в линейное смещение через объект layout-стратегии (row-major, column-major или произвольный кастомный layout).This is exactly the mechanism std::mdspan uses: an access like md[i, j, k] essentially calls an operator[] taking as many indices as the mdspan's extents describe, internally translating them into a linear offset via a layout-strategy object (row-major, column-major, or an arbitrary custom layout).

std::mdspan md(data.data(), 4, 4);  // 4x4 представление плоского буфера data// a 4x4 view over the flat data buffer
for (std::size_t i = 0; i < md.extent(0); ++i)
    for (std::size_t j = 0; j < md.extent(1); ++j)
        md[i, j] *= 2.0;  // естественный N-мерный синтаксис благодаря этой же фиче// natural N-dimensional syntax thanks to this same feature

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Матрица с прямой индексацией по строке и столбцуA matrix with direct row/column indexing

Базовый случай — плотная (dense) матрица поверх одномерного вектора, где раньше требовался либо operator(), либо proxy-строка.The basic case — a dense matrix over a one-dimensional vector, which used to require either operator() or a proxy row.

class DenseMatrix {
public:
    double& operator[](std::size_t row, std::size_t col) {
        return data_[row * cols_ + col];
    }
private:
    std::vector<double> data_;
    std::size_t cols_;
};
02

Трёхмерный тензор для обработки изображений/видеоA 3D tensor for image/video processing

Доступ к пикселю по (x, y, канал) — до C++23 либо тройная вложенная proxy-цепочка, либо operator() с тремя аргументами; сейчас — естественный тройной индекс в квадратных скобках.Pixel access by (x, y, channel) — before C++23, either a triple-nested proxy chain or a three-argument operator(); now, a natural triple index in square brackets.

class ImageTensor {
public:
    std::uint8_t& operator[](std::size_t x, std::size_t y, std::size_t channel) {
        return pixels_[(y * width_ + x) * channels_ + channel];
    }
private:
    std::vector<std::uint8_t> pixels_;
    std::size_t width_, channels_;
};

img[10, 20, 0] = 255;  // красный канал пикселя (10, 20)// the red channel of pixel (10, 20)
03

Разреженная (sparse) структура с составным ключомA sparse structure with a composite key

Разреженная матрица на основе хэш-таблицы, где составной индекс (i, j) естественно упаковывается в ключ без промежуточного proxy-объекта, обращающегося к хэш-таблице дважды.A hash-table-backed sparse matrix where the composite index (i, j) is naturally packed into the key without an intermediate proxy object hitting the hash table twice.

class SparseMatrix {
public:
    double& operator[](std::size_t i, std::size_t j) {
        return cells_[{i, j}];  // один прямой доступ к unordered_map с ключом-парой// one direct access to an unordered_map keyed by a pair
    }
private:
    std::unordered_map<std::pair<std::size_t, std::size_t>, double> cells_;
};
04

Обёртка над std::mdspan с дополнительной проверкой границA wrapper over std::mdspan with extra bounds checking

Собственный тип-обёртка, добавляющий debug-проверку границ поверх быстрого std::mdspan, сохраняя тот же самый синтаксис индексации, что и у оборачиваемого типа.A custom wrapper type adding debug bounds-checking on top of a fast std::mdspan, while keeping the exact same indexing syntax as the wrapped type.

template<class T>
class CheckedGrid {
public:
    T& operator[](std::size_t i, std::size_t j) {
        assert(i < span_.extent(0) && j < span_.extent(1));
        return span_[i, j];  // делегирует индексацию тем же синтаксисом// delegates indexing with the same syntax
    }
private:
    std::mdspan<T, std::dextents<std::size_t, 2>> span_;
};

ГраблиGotchas

m[i, j] на старом одноаргументном operator[] молча значит совсем другоеm[i, j] on an old single-argument operator[] silently means something else entirely

Это самая важная ловушка всей фичи. Если класс объявляет operator[] только с одним параметром (как это было единственно возможно до C++23), выражение m[i, j] по-прежнему компилируется — но означает совершенно другое: i и j разделены оператором запятая (comma operator), который вычисляет оба выражения по порядку и отбрасывает результат первого, передавая в одноаргументный operator[] только значение j. Это не ошибка компиляции — код молча меняет смысл, если библиотека не была явно переписана на новую многоаргументную сигнатуру. При смешивании старого и нового кода (например, заголовочный файл матричной библиотеки не обновлён, а вызывающий код уже написан в расчёте на C++23-семантику) это создаёт крайне трудноуловимый баг: обращение по адресу [j] вместо ожидаемого (i, j), без единого предупреждения компилятора в базовой конфигурации. Современные компиляторы обычно выдают предупреждение вроде -Wcomma-subscript, но полагаться на это нельзя — стоит явно проверять сигнатуру operator[] у используемого типа.This is the single most important trap in the whole feature. If a class declares operator[] with only one parameter (the only option possible before C++23), the expression m[i, j] still compiles — but means something entirely different: i and j are separated by the comma operator, which evaluates both expressions in order and discards the first one's result, passing only j's value into the single-argument operator[]. This isn't a compilation error — the code silently changes meaning if the library hasn't been explicitly rewritten for the new multi-argument signature. When mixing old and new code (e.g. a matrix library header hasn't been updated, but the calling code was already written assuming C++23 semantics), this creates an extremely hard-to-spot bug: indexing at [j] instead of the expected (i, j), with no compiler warning at all in a default configuration. Modern compilers usually emit a warning like -Wcomma-subscript, but that shouldn't be relied on — it's worth explicitly checking the operator[] signature of the type being used.

Явная запятая внутри [] по-прежнему возможна, но требует скобокAn explicit comma operator inside [] is still possible, but needs parens

Если действительно нужен классический оператор запятая внутри [] (крайне редкий, но легальный сценарий), его можно получить, обернув выражение в дополнительные скобки: m[(i, j)] — одно выражение в скобках, вычисляющее i, затем j, и передающее только j. Без внешних скобок компилятор трактует запятую как разделитель аргументов многоаргументного operator[], если такая перегрузка существует, или как classic comma-operator в противном случае — двусмысленность разрешается исключительно наличием подходящей перегрузки.If the classic comma operator inside [] is genuinely needed (an extremely rare but legal scenario), it can be obtained by wrapping the expression in extra parentheses: m[(i, j)] — one parenthesized expression that evaluates i, then j, passing only j. Without the outer parens, the compiler treats the comma as an argument separator for a multi-argument operator[] if such an overload exists, or as the classic comma operator otherwise — the ambiguity is resolved purely by the presence of a matching overload.

Перегрузка по количеству аргументов работает как у обычных функций, но без параметров по умолчанию в той же мере гибкостиOverloading by argument count works like ordinary functions, but without the same flexibility for default parameters

Можно объявить несколько перегрузок operator[] с разным числом параметров (например, отдельно для 2D и 3D доступа) — перегрузка разрешается стандартными правилами C++, как для обычных функций. Однако сочетание многоаргументного operator[] с параметрами по умолчанию и вариативными шаблонами сложнее в разрешении неоднозначностей, чем для обычной функции с тем же набором параметров — если нужна точность в пограничном случае, стоит свериться с cppreference или актуальным черновиком стандарта, а не полагаться на интуицию из обычных перегрузок функций.You can declare several operator[] overloads with different parameter counts (e.g. separate ones for 2D and 3D access) — overload resolution follows the standard C++ rules, same as for ordinary functions. However, combining a multi-argument operator[] with default parameters and variadic templates is trickier to disambiguate than for an ordinary function with the same parameter set — for precision in an edge case, it's worth checking cppreference or the current standard draft rather than relying on intuition carried over from ordinary function overloads.