Возможное расширение языка C++ операцией скалярного произведения

от автора

У меня возникла идея, как можно расширить синтаксис C++ операцией скалярного произведения. Если кратко, то произведение двух матриц в новых обозначениях будет выглядеть так:

C[>i][>j] = A[i][>k] * B[>k][j]; 

Насколько мне известно, сочетания операторов [> и [< вроде бы нигде не используются. Их можно применить для декларации индексов, которые существуют только в пределах данного выражения. Сочетание [> используется для декларации индекса, который пробегает от начала до конца массива в прямом направлении, а сочетание [< для декларации индекса, который пробегает в обратном направлении. Для повторяющихся индексов в произведении подразумевается суммирование — они аналогичны немым индексам в тензорных обозначениях.

Разберём на примерах, как это будет работать.

Скалярное произведение двух векторов

float a[3], b[3]; // ... float result = a[>i] * b[>i]; 

Здесь мы сначала декларируем конструкцией [>i] индекс i, который будет проходить значения 0,1, 2. После знака произведения повторная конструкция [>i] означает, что этот индекс — немой, и по нему будет идти суммирование.

Данный код эквивалентен следующему:

float a[3], b[3]; // ... float result = 0; for(int i = 0; i < 3; i++) result += a[i] * b[i]; 

Так как в конструкции a[>i] * b[>i] очевидно, по какому индексу производится суммирование, то допустима и сокращённая запись скалярного произведения, без явного указания имени индекса:

float a[3], b[3]; // ... float result = a[>] * b[>]; 

Явное задание размера массива

Так как компилятор не всегда может знать размер динамического массива, то этот размер при декларации индекса придётся указать явно. Это делается при помощи конструкции
[> index: expr]
где результатом выражения expr является размер массива:

float *a = new float[10]; float *b = new float[10]; // ... float result = a[>i: 10] * b[>i]; 

Здесь при первой декларации индекса i мы указали, что размер массива равен 10. При последующих декларациях индекса с тем же именем в том же выражении, размер указывать не обязательно.

Данный код эквивалентен следующему:

float *a = new float[10]; float *b = new float[10]; // ... float result = 0; for(int i = 0; i < 10; i++) result += a[i] * b[i]; 

Поэлементное присвоение вектору выражения

float a[10]; a[>i] = i * i; 

Здесь мы декларировали свободный индекс [>i], который говорит компилятору, что мы инициализируем каждый элемент вектора. В данном случае мы присваиваем каждому элементу вектора квадрат его индекса.

Данный код эквивалентен следующему:

float a[10]; for(int i = 0; i < 10; i++) a[i] = i * i; 

Если мы инициализируем вектор выражением, которое от индекса не зависит, то имя индекса можно не указывать:

float a[10]; a[>] = 0; 

Поэлементное произведение векторов

float a[3], b[3]; // ... float result[3]; result[>i] = a[i] * b[i]; 

В правой части последнего выражения индекс не является немым, и обрабатывается, как обычный индекс массива.
Данный код эквивалентен следующему:

float a[3], b[3]; // ... float result[3]; for(int i = 0; i < 3; i++) result[i] = a[i] * b[i];  

Также допустимо в правой части один раз декларировать индекс с помощью [>i]. Он всё равно будет рассматриваться, как свободный, и мы по-прежнему получим тот же результат:

float a[3], b[3]; // ... float result[3]; result[>i] = a[>i] * b[i]; // эквивалентно a[i] * b[i]; 

Реверсия

Формально, выражение присваивается не сразу, а через промежуточный временный объект. Это означает, что пока все итерации не будут завершены — предыдущие значения массивов в правой части выражения не пострадают. Такой подход позволяет простым способом записать переворот вектора (реверсию):

float a[10]; // ... a[>i] = a[<i]; 

Здесь в правой части через [<i] мы объявили, что индекс i отсчитывается в обратном направлении: для последнего элемента он будет = 0, для предпоследнего = 1, и так далее.
Данный код эквивалентен следующему:

float a[10]; // ... for(int i = 0; i < 5; i++) { float t = a[i]; a[i] = a[9 - i]; a[9 - i] = t; } 

При реверсии допустимо не указывать явно имя индекса:

float a[10]; // ... a[>] = a[<]; 

Умножение матриц

float A[3][3], B[3][3]; // ... float C[3][3];  C[>i][>j] = A[i][>k] * B[>k][j]; 

Здесь мы с помощью C[>i][>j] объявляем, что будем инициализировать все элементы матрицы C, затем вычисляем произведение матриц классическим образом: умножая строку матрицы A на столбец матрицы B.

Данный код эквивалентен следующему:

float A[3][3], B[3][3]; // ... float C[3][3];  for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) { float c = 0; for(int k = 0; k < 3; k++) c = A[i][k] * B[k][j]; C[i][j] = c; } 

При умножении зачастую выгодно предварительно транспонировать матрицу правого сомножителя. Это тоже просто записывается:

float A[3][3], B[3][3]; // ... float Bt[3][3];  Bt[>i][>j] = B[j][i]; // транспонирование float C[3][3];  C[>i][>j] = A[i][>k] * Bt[j][>k]; // эквивалентно A[i][>k] * B[>k][j]; 

Векторное произведение

Мы можем умножать более двух объектов.Это позволяет записать векторное произведение:

float e[3][3][3]; // псевдотензор Леви-Чивиты float a[3], b[3]; // ... e[>][>][>] = 0; e[0][1][2] = 1; e[1][0][2] = -1; e[1][2][0] = 1; e[2][1][0] = -1; e[2][0][1] = 1; e[0][2][1] = -1;  float result[3];  result[>i] = e[i][>j][>k] * a[>j] * b[>k]; 

Последнее выражение эквивалентно следующему коду:

for(int i = 0; i < 3; i++) { float r = 0; for(int j = 0; j < 3; j++) for(int k = 0; k < 3; k++) r += e[i][j][k] * a[j] * b[k]; result[i] = r; } 

Псевдотензор Леви-Чивиты можно сделать объектом стандартной библиотеки. Тогда компилятор сможет обнаруживать его использование, что упростит оптимизацию кода.

Пример из трёхмерной графики

Рассмотрим пусть и не оптимизированный, зато наглядный пример. Предположим, что некий трёхмерный объект задаётся массивом координат вершин, причём вершина, кроме координат, хранит и другие данные. Сначала мы масштабируем трёхмерный объект, причём в каждом направлении осей на свой коэффициент, затем умножаем на матрицу поворота, затем смещаем на какой-то вектор. Результатом получаем новый массив вершин.

// Вершина struct Vertex { float coor[3]; // координаты вершины // ... }; int vertexCount;  // количество вершин в объекте  float scale[3]; // вектор масштабирующих коэффициентов float rotation[3][3]; // матрица поворота float offset[3]; // вектор смещения // ... // Создаём исходный объект Vertex *srcObject = new Vertex[vertexCount]; // ... // Создаём объект назначения Vertex *destObject = new Vertex[vertexCount];  destObject[>vertex: vertexCount].coor[>x] =  srcObject[vertex].coor[>i] * scale[i] * rotation[>i][x] + offset[x]; 

Последнее выражение эквивалентно следующему коду:

for(int vertex = 0; vertex < vertexCount; vertex++) for(int x = 0; x < 3; x++) { float coor = 0; for(int i = 0; i < 3; i++) coor += srcObject[vertex].coor[i] * scale[i] * rotation[i][x]; destObject[vertex].coor[x] = coor + offset[x]; } 


ссылка на оригинал статьи https://habr.com/ru/articles/914458/