Как constexpr выводит значение? - PullRequest
1 голос
/ 22 апреля 2020

Я просматривал программу, упомянутую на cppreference.com для LiteralTypes. (https://en.cppreference.com/w/cpp/named_req/LiteralType)

Я знаю, что constexpr выводит значение во время компиляции. Но в приведенном ниже случае строки 10, 12 и 16 не знают входные параметры напрямую. (по крайней мере, я не могу разобрать)

Тогда как это вывести значение?

  1 #include <iostream>
  2 #include <stdexcept>
  3 
  4 class conststr
  5 {
  6     const char* p;
  7     std::size_t sz;
  8 public:
  9     template<std::size_t N>
 10     constexpr conststr(const char(&a)[N]) : p(a), sz(N - 1) {}
 11 
 12     constexpr char operator[](std::size_t n) const
 13     {
 14         return n < sz ? p[n] : throw std::out_of_range("");
 15     }
 16     constexpr std::size_t size() const { return sz; }
 17 };
 18 
 19 constexpr std::size_t countlower(conststr s, std::size_t n = 0,
 20                                              std::size_t c = 0)
 21 {
 22     return n == s.size() ? c :
 23            s[n] >= 'a' && s[n] <= 'z' ? countlower(s, n + 1, c + 1) :
 24                                         countlower(s, n + 1, c);
 25 }
 26 
 27 // output function that requires a compile-time constant, for testing
 28 template<int n>
 29 struct constN
 30 {
 31     constN() { std::cout << n << '\n'; }
 32 };
 33 
 34 int main()
 35 {
 36     std::cout << "the number of lowercase letters in \"Hello, world!\" is ";
 37     constN<countlower("Hello, world!")>(); // implicitly converted to conststr
 38 }

1 Ответ

2 голосов
/ 22 апреля 2020

Когда достигается строка 37 constN<countlower("Hello, world!")>();, компилятор пытается вывести значение и заменить его на месте.

Таким образом, компилятор вызывает функцию countlower("Hello, world!"). Параметры std::size_t n = 0, std::size_t c = 0 затем устанавливаются в их значения по умолчанию, поскольку они не были переданы.

Тело функции состоит из рекурсии return n == s.size() ? c : s[n] >= 'a' && s[n] <= 'z' ? countlower(s, n + 1, c + 1) : countlower(s, n + 1, c);, где параметры n и c увеличиваются на каждой итерации.

n - это индекс для обозначения позиции персонажа, который в данный момент тестируется. c обозначает количество строчных букв.

Когда n достигает конечного индекса, все рекурсивные вызовы возвращают значение и достигается окончательное значение. Это значение передается в качестве аргумента шаблона, определенного в строке 28 template<int n>, и создается новый объект constN.

Это все делается во время компиляции.

Второй просмотр

Представьте компилятор как другую C++ программу, которая определяет рекурсивную функцию, которая подсчитывает количество младших символов в переданной строке и возвращает объект с номером в качестве члена.

Итак, это:

constN<countlower("Hello, world!")>();

Затем заменяется следующим:

constN<9>();

Конструктор

ОК. Итак, давайте представим структуру constN как обычную структуру или класс, подобный этому:

struct constN
{
    int n;

    // constructor for the object taking one argument
    constN(int n) : n(n) {};
}

После случайного вызова, такого как constN(9), мы получаем объект с n = 9. Теперь аргументы шаблона просто такие, но вместо этого вы передаете их в острых скобках <>.

Итак, они равны:

struct CasualObject
{
    int n;

    CasualObject(int n) : n(n) {};
}

template<int n>
struct YourObject
{
    YourObject() { std::cout << n << '\n'; }
};

CasualconstN(9) == YourconstN<9>()

Теперь давайте скажем, что функция countlower просто нормальная функция, которая возвращает некоторое целое число. Таким образом, вы можете вызвать функцию до создания объекта, который передает результат функции конструктору.

Итак, они равны:

int a = countlower("Hey");
constN obj1(a);

constN obj2(countlower("Hey"));

obj1 == obj2;

В конце компилятор создает объект с n = countlower("Hello, world!"). Теперь обратим внимание на единственный метод, который constN определил в строке 31:

constN() { std::cout << n << '\n'; }

Wow. Это конструктор. Он имеет то же имя, что и тип объекта. Так что мы не только вызываем конструктор с n = 9, но и выполняем его тело. Это означает, что n выводится на консоль.

Наконец, объект constN не назначен ни одной переменной. Это означает, что на него больше нельзя ссылаться. Умный компилятор, вероятно, удалит строку 37 все вместе и заменит ее простым оператором print:

cout << 9 << '\n; // В «Hello, world!» Есть 9 строчных букв `

Неявное преобразование

Итак, вопрос такой: Как компилятор узнает, что должно быть N, когда конструирование conststr?

Для иллюстрации я создал небольшую программу:

#include <iostream>

class conststr
{
    const char* p;
    std::size_t sz;
public:
    template<std::size_t N>
    constexpr conststr(const char(&a)[N]) : p(a), sz(N - 1) {}

    constexpr std::size_t size() const { return sz; }
};

int main()
{
    char a[4] = "Hey";
    const char b[4] = "Hey";

    conststr x(a);
    conststr y(b);
    conststr z("Hey");

    printf("%lu %lu %lu", x.size(), y.size(), z.size());
    return 0;
}

Теперь, если вы запустите это, вы получите вывод 3 3 3. Но я тут кричу: «В коде только 4, и последний объект не имеет объявленного размера». Давайте расшифруем его по крупицам:

Сначала мы создадим несколько строк с типами char array и const char array (по сути, указатели).

char a[4] = "Hey";
const char b[4] = "Hey";

Они содержат 3 буквы и нулевой терминатор \0, что составляет 4 символа. Когда мы создаем первый conststr объект:

conststr x(a);

Итак, мы передаем переменную a, которая имеет тип char []. char [] можно преобразовать в const char[]. По сути, то же самое только с модификатором const. Он также может быть преобразован в std::string и многие другие. Так что компилятор считает это очень похожим. Итак, мы определили этот бит кода конструктора:

conststr(const char(&a))
// which can be converted to all of these:
conststr(const char a[])
conststr(char* a)
conststr(char (&a))

Но есть определенный шаблон:

template<std::size_t N>
conststr(const char(&a)[N])

Чтобы определить, каким должен быть N, компилятор пытается переписать определение параметра a в соответствии с потребностями функций. Это называется неявным преобразованием и имеет некоторые правила:

  • Если переданные параметры соответствуют типу, это нормально
  • Если нет, попробуйте преобразование
  • Если есть преобразование, примените его
  • Если преобразование переданного типа в тип аргумента неизвестно во время компиляции, выведите ошибку компиляции
// so from main() we have:
char a[4] = "Hey";
// this can be rewritten like so:
const char a[4] = "Hey";

// now it looks very similar to the definition of the constructor:
const char(&a)[N]
const char a[4]

Как я показал ранее, это равны. Поэтому теперь компилятор может взять то, что в скобках, и поместить его вместо N.

ОК. Но это не 3 ... Если мы заглянем внутрь «тела» конструктора, мы увидим, что size sz присвоено значение N - 1. И это наши 3.

conststr(const char(&a)[N]) : p(a),  sz(N - 1)
conststr(const char a[4]): p("Hey"), sz(4 - 1)

Теперь шаблоны, такие как template<std::size_t N>, просто говорят компилятору, что значение должно быть вычислено или преобразовано во время компиляции. Таким образом, вы не можете создать свой собственный N, он всегда зависит от длины передаваемой строки.

Хорошо, но как насчет этого:

conststr z("Hey");

Ну, опять же, Компилятор пытается преобразовать параметр в подходящий тип. И поскольку это занимает const char a[], оно будет преобразовано в это. И я уже освещал это.

...