Как работает эта реализация std:: is_class?

Я пытаюсь понять реализацию std::is_class. Я скопировал несколько возможных реализаций и скомпилировал их, надеясь выяснить, как они работают. После этого я обнаружил, что все вычисления выполняются во время компиляции (как я должен был выяснить раньше, оглядываясь назад), поэтому gdb не может дать мне больше подробностей о том, что именно происходит.

Вот реализация, которую я изо всех сил пытаюсь понять:

template<class T, T v>
    struct integral_constant{
    static constexpr T value = v;
    typedef T value_type;
    typedef integral_constant type;
    constexpr operator value_type() const noexcept {
        return value;
    }
};

namespace detail {
    template <class T> char test(int T::*);   //this line
    struct two{
        char c[2];
    };
    template <class T> two test(...);         //this line
}

//Not concerned about the is_union<T> implementation right now
template <class T>
struct is_class : std::integral_constant<bool, sizeof(detail::test<T>(0))==1 
                                                   && !std::is_union<T>::value> {};

У меня проблемы с двумя закомментированными строками. Эта первая строка:

 template<class T> char test(int T::*);

Что означает T::*? Кроме того, это не объявление функции? Он выглядит как единое целое, но компилируется без определения тела функции.

Вторая строка, которую я хочу понять:

template<class T> two test(...);

Еще раз, не является ли это объявлением функции без определения тела? Кроме того, что означает многоточие в этом контексте? Я думал, что многоточие в качестве аргумента функции требует один определенный аргумент перед ...?

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

Ссылки:

Ответ 1

То, что вы смотрите, это некоторая технология программирования под названием "SFINAE", которая означает "Ошибка замены - это не ошибка". Основная идея такова:

namespace detail {
  template <class T> char test(int T::*);   //this line
  struct two{
    char c[2];
  };
  template <class T> two test(...);         //this line
}

Это пространство имен содержит 2 перегрузки для test(). Оба являются шаблонами, разрешенными во время компиляции. Первый принимает аргумент int T::*. Он называется Member-Pointer и является указателем на int, но для int является членом класса T. Это только допустимое выражение, если T - класс. Второй принимает любое количество аргументов, которое действительно в любом случае.

Итак, как он используется?

sizeof(detail::test<T>(0))==1

Хорошо, мы передаем функцию a 0 - это может быть указатель, и особенно указатель-член, - никакая информация не получена, которая перегружается для использования из этого. Поэтому, если T является классом, мы могли бы использовать здесь как перегрузку T::*, так и ..., а так как перегрузка T::* является более конкретной здесь, она используется. Но если T не является классом, то мы не можем иметь что-то вроде T::*, а перегрузка плохо сформирована. Но это сбой, который произошел во время замены шаблона. А поскольку "сбой замены" не является ошибкой, компилятор молча игнорирует эту перегрузку.

Затем применяется sizeof(). Заметили различные типы возврата? Поэтому в зависимости от T компилятор выбирает правильную перегрузку и, следовательно, правильный тип возврата, что приводит к размеру либо sizeof(char), либо sizeof(char[2]).

И, наконец, поскольку мы используем только размер этой функции и на самом деле не называем ее, нам не нужна реализация.

Ответ 2

Часть того, что вас путает, что пока не объяснено другими ответами, заключается в том, что функции test никогда не называются фактически. Тот факт, что у них нет определений, не имеет значения, если вы их не называете. Как вы поняли, все это происходит во время компиляции, без запуска какого-либо кода.

Выражение sizeof(detail::test<T>(0)) использует оператор sizeof в выражении вызова функции. Операнд sizeof является необоснованным контекстом, что означает, что компилятор фактически не выполняет этот код (т.е. Оценивает его, чтобы определить результат). Нет необходимости вызывать эту функцию, чтобы узнать sizeof, каким будет результат, если вы его вызвали. Чтобы узнать размер результата, компилятору нужно только просмотреть объявления различных функций test (чтобы узнать их типы возврата), а затем выполнить разрешение перегрузки, чтобы увидеть, какой из них будет вызываться, и, следовательно, чтобы найти, что sizeof результат будет.

Остальная часть головоломки заключается в том, что вызов неоплаченной функции detail::test<T>(0) определяет, можно ли использовать T для формирования типа "указатель-член" int T::*, что возможно только в том случае, если T - тип класса (потому что не-классы не могут иметь членов, и поэтому не могут иметь указателей на своих членов). Если T - класс, тогда может быть вызвана первая перегрузка test, в противном случае вызывается вторая перегрузка. Вторая перегрузка использует список параметров printf -style..., что означает, что он принимает что-либо, но также считается худшим, чем любая другая жизнеспособная функция (иначе функции, использующие..., будут слишком "жадными" и будут вызваны всеми время, даже если существует более конкретная функция t hat точно соответствует аргументам). В этом коде функция... является возвратом для "если ничего другого не подходит, вызовите эту функцию", поэтому, если T не является типом класса, используется резервное копирование.

Не имеет значения, действительно ли тип класса имеет переменную-член типа int, он действительно имеет тип int T::* в любом случае для любого класса (вы просто не могли бы сделать этот указатель-к-член обратитесь к любому члену, если тип не имеет члена int).

Ответ 3

Что означает T::*? Кроме того, это не объявление функции? Он выглядит как один, но это компилируется без определения тела функции.

int T::* является указателем на объект-член. Его можно использовать следующим образом:

struct T { int x; }
int main() {
    int T::* ptr = &T::x;

    T a {123};
    a.*ptr = 0;
}

Еще раз, это не объявление функции без какого-либо тела? Что же означает в этом контексте многоточие?

В другой строке:

template<class T> two test(...);

эллипсис представляет собой конструкцию C, чтобы определить, что функция принимает любое количество аргументов.

Я хотел бы понять, что делает этот код.

В основном он проверяет, является ли конкретный тип struct или class, проверяя, можно ли интерпретировать 0 как указатель на член (в этом случае T - тип класса).

В частности, в этом коде:

namespace detail {
    template <class T> char test(int T::*);
    struct two{
        char c[2];
    };
    template <class T> two test(...);
}

у вас есть две перегрузки:

  • который сопоставляется только тогда, когда a T является типом класса (в этом случае это одно из лучших совпадений и "выигрывает" над вторым)
  • который сопоставляется каждый раз

В первом sizeof результат дает 1 (возвращаемый тип функции char), другой дает 2 (структура, содержащая символы 2).

Проверяемое логическое значение:

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value

что означает: return true, только если интегральная константа 0 может быть интерпретирована как указатель на член типа T (в этом случае это тип класса), но это не a union (который также является возможным типом класса).

Ответ 4

Test - это перегруженная функция, которая либо берет указатель на член в T, либо что угодно. С++ требует использования наилучшего совпадения. Поэтому, если T - тип класса, он может иметь член в нем... тогда эта версия выбрана, а размер ее возврата равен 1. Если T не является типом класса, тогда T:: * делает нулевой смысл, так что версия функция отфильтрована SFINAE и не будет там. Используется любая версия, и тип возвращаемого типа не равен 1. Таким образом, проверка размера возврата вызова этой функции приводит к решению, может ли тип иметь членов. Единственное, что осталось - убедиться, что это не союз, чтобы решить если это класс или нет.

Ответ 5

Черта типа std::is_class выражается через встроенную функцию компилятора (называемую __is_class в большинстве популярных компиляторов) и не может быть реализована в "нормальном" C++.

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

Вот пример, который можно воспроизвести на любом компиляторе msvc x64.

Предположим, я написал свою собственную реализацию is_class:

namespace detail
{
    template<typename T>
    constexpr char test_my_bad_is_class_call(int T::*) { return {}; }

    struct two { char _[2]; };

    template<typename T>
    constexpr two test_my_bad_is_class_call(...) { return {}; }
}

template<typename T>
struct my_bad_is_class
    : std::bool_constant<sizeof(detail::test_my_bad_is_class_call<T>(nullptr)) == 1>
{
};

Давайте попробуем:

class Test
{
};

static_assert(my_bad_is_class<Test>::value == true);
static_assert(my_bad_is_class<const Test>::value == true);

static_assert(my_bad_is_class<Test&>::value == false);
static_assert(my_bad_is_class<Test*>::value == false);
static_assert(my_bad_is_class<int>::value == false);
static_assert(my_bad_is_class<void>::value == false);

Пока тип T полностью определен к моменту, когда my_bad_is_class применяется к нему в первый раз, все будет хорошо. И размер указателя на функцию-член останется таким, каким он должен быть:

// 8 is the default for such simple classes on msvc x64
static_assert(sizeof(void(Test::*)()) == 8);

Однако, все становится довольно "интересно", если мы используем нашу особенность пользовательских типов с заранее объявленным (и еще не определенным) типом:

class ProblemTest;

Следующая строка неявно запрашивает тип int ProblemTest::* для заранее объявленного класса, определение которого компилятором сейчас не видно.

static_assert(my_bad_is_class<ProblemTest>::value == true);

Это компилирует, но неожиданно нарушает размер указателя на функцию-член.

Кажется, что компилятор пытается "создать" (аналогично тому, как создаются шаблоны) размер указателя на функцию-член ProblemTest в тот же момент, когда мы запрашиваем тип int ProblemTest::* в нашей реализации my_bad_is_class. И в настоящее время компилятор не может знать, каким он должен быть, поэтому у него нет другого выбора, кроме как принять максимально возможный размер.

class ProblemTest // definition
{
};

// 24 BYTES INSTEAD OF 8, CARL!
static_assert(sizeof(void(ProblemTest::*)()) == 24);

Размер указателя на функцию-член был утроен! И его нельзя сжать обратно даже после того, как определение класса ProblemTest было замечено компилятором.

Если вы работаете с некоторыми сторонними библиотеками, которые полагаются на определенные размеры указателей на функции-члены вашего компилятора (например, знаменитый FastDelegate от Don Clugston), такие неожиданные изменения размера, вызванные некоторым вызовом свойства типа, могут быть настоящей болью. Прежде всего потому, что вызовы признаков типа не должны ничего изменять, но в данном конкретном случае они это делают - и это крайне неожиданно даже для опытного разработчика.

С другой стороны, если бы мы реализовали наш is_class с помощью встроенного __is_class, все было бы хорошо:

template<typename T>
struct my_good_is_class
    : std::bool_constant<__is_class(T)>
{
};

class ProblemTest;

static_assert(my_good_is_class<ProblemTest>::value == true);

class ProblemTest
{
};

static_assert(sizeof(void(ProblemTest::*)()) == 8);

В этом случае вызов my_good_is_class<ProblemTest> не нарушает никаких размеров.

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

Ответ 6

Вот стандартная формулировка:

[Expr.sizeof]:

Оператор sizeof возвращает число байтов, занятых непересекающимся объектом типа его операнда.

Операнд является либо выражением, которое является неоцененным операндом ([Expr.prop])......

2. [expr.prop]:

In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp]).

Неоцененный операнд не оценивается.

3. [temp.fct.spec]:

  1. [Примечание: удержание типа может завершиться ошибкой по следующим причинам:

...

(11.7) Попытка создать "указатель на член T", когда T не является типом класса. [Пример:

  template <class T> int f(int T::*);
  int i = f<int>(0);

- конец примера ]

Как показано выше, это хорошо определено в стандарте :-)

4. [dcl.meaning]:

[Example:

struct X {
void f(int);
int a;
};
struct Y;

int X::* pmi = &X::a;
void (X::* pmf)(int) = &X::f;
double X::* pmd;
char Y::* pmc;

объявляет pmi, pmf, pmd и pmc как указатель на член X типа int, указатель на член X типа void (int), указатель на член X типа double и указатель на член Y типа char соответственно.Объявление pmd корректно сформировано, хотя X не имеет членов типа double. Аналогично, объявление pmc корректно сформировано, даже если Y является неполным типом.