Введение


Недавно ко мне подошли с вопросом, «что это и как этим пользоваться?», показав следующий код:

extern "C" {
    void byteMaskDowngrade(byte***const byteMask, const byte *const *const && source) {
        // какой-то код.
    }

    // некоторые фукции.
}

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

И в упреждение возможных проблем у коллег по цеху, я решил создать данную статью, полагая её в качестве шпаргалки, а именно ответов на два вопроса:

  1. Как правильно писать такие вот объявления?
  2. И как их правильно читать?

"*"


Случай с одной звёздочкой самый распространённый, однако и здесь возможны недопонимания:

  • Как правильно:

    • Так:

      const byte p;
    • Или так:

      byte const p;

  • Или в случае с записью '*':

    • Так:

      byte *const p;
    • Или так:

      const byte *p;

Строго говоря, с точки зрения компилятора, наличие звёздочки и её положения в выражении ставит вопрос ребром, т.к. итоговый смысл записи будет отличаться. Наличие спецификатора const в выше указанном случае не имеет никакой разницы при учёте его позиции относительно типа данных, однако ситуация кардинально меняется, когда появляются указатели: один, два, три… ну вы поняли:

void p() {
    // указатель на байт.
    byte * a = new byte{1};
    a++; // допустимо.
    a[0]++; // допустимо.

    // указатель на константный байт.
    const byte * b = new byte{1};
    b++; // допустимо.
    //b[0]++; // недопустимо.

    // константный указатель на байт.
    byte *const c = new byte{1};
    //c++; // недопустимо.
    c[0]++; // допустимо.

    // константный указатель на константный байт.
    const byte *const d = new byte{1};
    //d++; // недопустимо.
    //d[0]++; // недопустимо.
}

Всё это выглядит интереснее, когда появляется второй указатель (здесь уже начинает прослеживаться эффективное для чтения правило записи):

void pp() {
    // указатель на указатель на байт.
    byte ** a = new byte * { new byte{1} };
    a++; // допустимо.
    a[0]++; // допустимо.
    a[0][0]++; // допустимо.

    // указатель на указатель на константный байт.
    const byte ** b = new const byte * { new byte{1}};
    b++; // допустимо.
    b[0]++; // допустимо.
    //b[0][0]++; // недопустимо.

    // указатель на константный указатель на байт.
    byte *const * c = new byte * { new byte{1}};
    c++; // допустимо.
    //c[0]++; // недопустимо.
    c[0][0]++; // допустимо.

    // константный указатель на указатель на байт.
    byte * *const d = new byte * { new byte{1}};
    //d++; // недопустимо.
    d[0]++; // допустимо.
    d[0][0]++; // допустимо.

    // указатель на константный указатель на константный байт.
    const byte *const * e = new const byte *const { new byte{1}};
    e++; // допустимо.
    //e[0]++; // недопустимо.
    //e[0][0]++; // недопустимо.

    // константный указатель на указатель на константный байт.
    const byte * *const f = new const byte * { new byte{1}};
    //f++; // недопустимо.
    f[0]++; // допустимо.
    //f[0][0]++; // недопустимо.

    // константный указатель на константный указатель на байт.
    byte *const *const g = new byte *const { new byte{1}};
    //g++; // недопустимо.
    //g[0]++; // недопустимо.
    g[0][0]++; // допустимо.

    // константный указатель на константный указатель на константный байт.
    const byte *const *const h = new const byte *const { new byte{1}};
    //h++; // недопустимо.
    //h[0]++; // недопустимо.
    //h[0][0]++; // недопустимо.
}

Как очевидно двойной указатель может быть представлен аж 8-мью различными способами, каждый из которых определяет особый способ использования целевых данных.

Правила чтения подобных выражений следующие:

  • ищем в выражении знак '=' и читаем выражение справа налево;
  • пропускаем имя переменной;
  • далее встречам либо '*', что означает обычный указатель, либо '*const' — константный указатель;
  • таким образом читаем до тех пор пока не встретится тип данных (byte);
  • и последним словом слева от типа данных может быть const, наличие которого означает, что вся эта конструкция ссылается на данные которые нельзя изменять; если же const нет — то можно.

Подобная форма записи и чтения позволяет с лёгкостью читать и понимать даже самые изысканные выражения)

Вот Вам для примера полный набор выражений с тройным указателем:

"***"


void ppp() {
    // указатель на указатель на указатель на байт.
    byte *** a = new byte * * { new byte * {new byte{1}} };
    a++; // допустимо.
    a[0]++; // допустимо.
    a[0][0]++; // допустимо.
    a[0][0][0]++; // допустимо.

    // указатель на указатель на указатель на константный байт.
    const byte *** b = new const byte * * { new const byte * {new byte{1}} };
    b++; // допустимо.
    b[0]++; // допустимо.
    b[0][0]++; // допустимо.
    //b[0][0][0]++; // недопустимо.

    // указатель на указатель на константный указатель на байт.
    byte*const * * c = new byte *const * { new byte *const {new byte{1}} };
    c++; // допустимо.
    c[0]++; // допустимо.
    //c[0][0]++; // недопустимо.
    c[0][0][0]++; // допустимо.

    // указатель на константный указатель на указатель на байт.
    byte * *const * d = new byte * *const { new byte * {new byte{1}} };
    d++; // допустимо.
    //d[0]++; // недопустимо.
    d[0][0]++; // допустимо.
    d[0][0][0]++; // допустимо.

    // константный указатель на указатель на указатель на байт.
    byte *** const e = new byte * * { new byte * {new byte{1}} };
    //e++; // недопустимо.
    e[0]++; // допустимо.
    e[0][0]++; // допустимо.
    e[0][0][0]++; // допустимо.

    // указатель на указатель на константный указатель на константный байт.
    const byte *const * * f = new const byte *const * { new const byte *const {new byte{1}} };
    f++; // допустимо.
    f[0]++; // допустимо.
    //f[0][0]++; // недопустимо.
    //f[0][0][0]++; // недопустимо.

    // указатель на константный указатель на указатель на константный байт.
    const byte * *const * g = new const byte * *const{ new const byte * {new byte{1}} };
    g++; // допустимо.
    //g[0]++; // недопустимо.
    g[0][0]++; // допустимо.
    //g[0][0][0]++; // недопустимо.

    // константный указатель на указатель на указатель на константный байт.
    const byte * * *const h = new const byte * *{ new const byte * {new byte{1}}};
    //h++; // недопустимо.
    h[0]++; // допустимо.
    h[0][0]++; // допустимо.
    //h[0][0][0]++; // недопустимо.

    // константный указатель на указатель на константный указатель на байт.
    byte *const * *const i = new byte *const * { new byte *const {new byte{1}}};
    //i++; // недопустимо.
    i[0]++; // допустимо.
    //i[0][0]++; // недопустимо.
    i[0][0][0]++; // допустимо.

    // константный указатель на константный указатель на указатель на байт.
    byte * *const *const j = new byte * *const { new byte * {new byte{1}}};
    //j++; // недопустимо.
    //j[0]++; // недопустимо.
    j[0][0]++; // допустимо.
    j[0][0][0]++; // допустимо.

    // указатель на константный указатель на константный указатель на байт.
    byte *const *const * k = new byte *const *const {new byte *const{new byte{1}}};
    k++; // допустимо.
    //k[0]++; // недопустимо.
    //k[0][0]++; // недопустимо.
    k[0][0][0]++; // допустимо.

    // здесь ещё надо случаи с когда в выражении присутствуют три const

    // константный указатель на константный указатель на константный указатель на константный байт.
    const byte *const *const *const m = new const byte *const *const {new const byte *const {new byte{1}}};
    //m++; // недопустимо.
    //m[0]++; // недопустимо.
    //m[0][0]++; // недопустимо.
    //m[0][0][0]++; // недопустимо.
}

Комментарии (19)


  1. yaroslavche
    23.09.2019 23:41
    +2

    Крайне интересно! Но зачем? Какой тайный смысл этого, и можно ли обойтись без указателя на указатель на указатель?


    1. dipsy
      24.09.2019 04:59

      Особенно второй параметр интересен, rvalue-ссылка на указатель это вообще зачем? Или это какой-то особый диалект языка, ссылка на ссылку?


      1. Tsvetik
        24.09.2019 13:27

        Тоже не пойму что это за && в Си


        1. mayorovp
          24.09.2019 15:15
          +1

          extern "C" намекает, что это не Си, а С++. А в плюсах && как тип данных имеет только одно значение.


          1. dipsy
            24.09.2019 16:57

            extern "C" вроде же как раз намекает прямо говорит, что функция использует соглашения линковки С, обычно она где-то в *.c файле реализована. Хотя ничего не мешает наверное и С++-функцию так объявить.


            1. mayorovp
              24.09.2019 17:11
              +1

              Где функция реализована — совершенно не важно. Важно лишь, что компилятор Си посчитает такую конструкцию синтаксической ошибкой — а значит, если я вижу extern "C", значит я смотрю на C++.


      1. mayorovp
        24.09.2019 15:22

        Константная rvalue-ссылка на указатель! Два раза "это вообще зачем?"...


    1. old_gamer
      24.09.2019 13:32

      3х мерный массив? )
      просто подумал, для чего в принципе можно это использовать…


  1. crea7or
    23.09.2019 23:43
    +1

    Не надо так ©


  1. AEP
    23.09.2019 23:46
    +1

    Чтобы читать такое, давным-давно придумали утилиту cdecl. И еще вот web-версия на JavaScript.


    1. Taraflex
      24.09.2019 06:32

    1. TargetSan
      24.09.2019 11:15

      Это просто шикарно — отдельная утилита чтобы парсить объявления типов в человекочитаемый вид.


    1. thatsme
      24.09.2019 14:17

      void byteMaskDowngrade(byte***const byteMask, const byte *const *const && source);

      syntax error

      void byteMaskDowngrade(byte***const byteMask, const byte *const *const && source){}


      bad character '{'


  1. loginsin
    24.09.2019 00:21
    +2

    А еще давным давно изобрели typedef. Лучше лестница из typedef'ов, чем это.
    Если только целью не была обфускация.


  1. Wyrd
    24.09.2019 01:03
    +1

    На всякий случай, если вдруг прошло мимо вас, есть замечательное «правило чтения по спирали»


    https://m.habr.com/ru/post/100104/


  1. merhalak
    24.09.2019 02:32

    Меня больше интересует другой вопрос: размеры структур по указателям какой размер имеют? И нет ли там в коде выхода за границы массива?


  1. KonstantinSpb
    24.09.2019 12:41

    Спойлер...


  1. Kabdim
    24.09.2019 15:59
    +4

    Зашел только ради того что бы узнать зачем была такая конструкция в продакшене… и ответа на этот вопрос здесь нет.


    1. UberSchlag
      24.09.2019 19:38

      Аналогично. Безусловно, ещё раз напомнить об этой теме не помешает, но описание правила чтения таких объявлений (и для указателей на функции тоже) есть в хороших вводных книгах по плюсам.
      А вот тайну конкретного использования такого уровня непрямого обращения хотелось бы узнать.