Неявное преобразование класса шаблона Variadic

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

using P = Policy<int, double, UserDefined>

Или что-то в этом роде. Я получил его для компиляции, но предостережение в том, что если double и int (или что-то, во что double может быть преобразовано на самом деле) являются параметрами шаблона, double будет неявно преобразован.

Политика выглядит так:

template <typename... T>
class BasicValidationPolicy { };

template <>
class BasicValidationPolicy<>
{
public:
    void RegisterSetHandler();
};

template <typename T, typename... Rest>
class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
{
public:
    using SetHandler = std::function<void(int, T)>;
    
    void RegisterSetHandler(const SetHandler& handler)
    {
        m_setHandler = handler;
    }
    
    void Set(int n, const T& val) {
        if (m_setHandler) {
            m_setHandler(n, val);
        }
    }
    
private:
    SetHandler m_setHandler{nullptr};
};

Класс, который его использует...

template <typename ValidatorPolicy>
class MyClass : public ValidatorPolicy  {
public:

    void OnSetInt(int n, int64_t v)
    {
        ValidatorPolicy::Set(n, v);
    }
    
    void OnSetDouble(int n, double d)
    {
        ValidatorPolicy::Set(n, d);
    }
};

Использование:

int main()
{
    using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
    MyClass<Policy> m;
    
    m.Policy::RegisterSetHandler([](int i, double value) {
        // by this point value is an int64_t
        std::cout << "Got double " << i << ", " << value << "\n";
    });
   
    double d{35.2135}; 
    m.OnSetDouble(1, d);
}

Для загрузки это работает

using Policy = BasicValidationPolicy<double, int64_t>;

Так что я думаю, что я что-то упустил в выводе шаблона. Похоже, что он пытается сопоставить double с std::int64_t, говорит «хм, достаточно хорошо» и движется дальше. Приятно знать способ обойти это (отчасти), но похоже, что это будет очень сложно поддерживать.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
68
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Все сложно...

Прежде всего: у вас есть рекурсивный класс-шаблон BasicValidationPolicy, в котором вы определяете два метода и хотите, чтобы все методы для всех шагов рекурсии класса были доступны.

К сожалению, определение методов в производных классах скрывает методы в базовых классах.

Чтобы скрыть унаследованные методы, вы должны явно добавить пару using

using BasicValidationPolicy<Rest...>::Set;
using BasicValidationPolicy<Rest...>::RegisterSetHandler;

На данный момент код не компилируется, потому что вам нужны Set() и RegisterSetHandler() в классе основного случая. Вас объявили пустышкой RegisterSetHandler(), но не пустышкой Set(). Вы должны добавить один, чтобы основной случай стал

template <>
class BasicValidationPolicy<>
{
public:
    void RegisterSetHandler();
    void Set();
};

Теперь ваш объект MyClass<Policy> предоставляет два метода RegisterSetHandler() (до одного): один получает std::function<void(int, std::int64_t)>, другой (до того, как скрытый) получает std::function<void(int, double)>.

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

Возможное решение - наложить преобразование лямбда/std::function в вызов

// ..........................VVVVVVVVVVVVVV
m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
                             // by this point value is an int64_t
                             std::cout << "Got double " << i << ", " << value << "\n";
                             }});
// ...........................^

используя также руководства по дедукции шаблонов, представленные в C++17.

Таким образом, ваш код становится

#include <iostream>
#include <functional>

template <typename... T>
class BasicValidationPolicy { };

template <>
class BasicValidationPolicy<>
{
public:
    void RegisterSetHandler();
    void Set();
};

template <typename T, typename... Rest>
class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
{
public:
    using SetHandler = std::function<void(int, T)>;

    using BasicValidationPolicy<Rest...>::Set;
    using BasicValidationPolicy<Rest...>::RegisterSetHandler;
    
    void RegisterSetHandler(const SetHandler& handler)
    {
        m_setHandler = handler;
    }
    
    void Set(int n, const T& val) {
        if (m_setHandler) {
            m_setHandler(n, val);
        }
    }
    
private:
    SetHandler m_setHandler{nullptr};
};

template <typename ValidatorPolicy>
class MyClass : public ValidatorPolicy  {
public:

    void OnSetInt(int n, int64_t v)
    {
        ValidatorPolicy::Set(n, v);
    }
    
    void OnSetDouble(int n, double d)
    {
        ValidatorPolicy::Set(n, d);
    }
};


int main ()
 {
   using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
   MyClass<Policy> m;
    
   m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
                                std::cout << "Got double " << i << ", " << value << "\n";
                                }});
   
    double d{35.2135}; 
    m.OnSetDouble(1, d);
}

Rockstar! Я делаю все возможное, чтобы не отставать от этой ошибки, там так много чертовых деталей. Когда-нибудь я пройдусь по книге Николаиса, но боюсь, что большая ее часть не приживется. Похоже, что лямбда-выражения иногда бывают ложными богами... Я знаю, что они не совпадают с std::function, но я бы никогда не подумал о передаче std::function таким образом... теперь это имеет смысл. В любом случае, спасибо!

kiss-o-matic 12.12.2020 00:39

@kiss-o-matic - «Похоже, что лямбды иногда бывают ложными богами ...» - Ламбды чрезвычайно полезны. Просто учтите, что у каждого инструмента есть свой предел.

max66 12.12.2020 12:14

Действительно. Я использую их часто... просто нужно помнить, что они не панацея.

kiss-o-matic 13.12.2020 08:28

Есть небольшая альтернатива рекурсивному определению, с которой может быть проще работать...

template<typename T>
class ValidationPolicy {
  // Set/Register/etc
};

template <typename... Ts>
class BasicValidationPolicy : public ValidationPolicy<Ts>... {
 public:
  using ValidationPolicy<Ts>::Set...;
  using ValidationPolicy<Ts>::RegisterSetHandler...;
};

Это может иметь некоторое влияние на время компиляции и другие аспекты разработки, хотя, скорее всего, относительно незначительное. Например, если у вас есть десятки классов, используемых в сотнях различных комбинаций политик в вашем приложении, рекурсивное определение приведет к тому, что для его поддержки будет гораздо больше различных типов и более крупных двоичных файлов. Например, в рекурсивном определении BasicValidationPolicy<T1, T2, T3> и BasicValidationPolicy<T3, T2, T1> использование будет генерировать 7 различных типов в иерархии (пустой используется в обоих расширениях). То же самое в более плоской иерархии будет 5 различных типов — по одному для каждого из T1, T2, T3 и по одному для каждой комбинации. Добавление BasicValidationPolicy<T2, T3, T1> рекурсивно добавит еще 3 типа, но еще 1 тип в плоской форме.

Ответ от @ max66 не является неправильным, просто есть над чем подумать.

Действительно. Можете проверить это, но время компиляции действительно является серьезной проблемой с моей текущей кодовой базой (о чем я регулярно информируюсь в MR). Я сомневаюсь, что это подтолкнет нас к краю, но есть над чем подумать.

kiss-o-matic 13.12.2020 08:30

@kiss-o-matic - это решение более элегантное, понятное и удобное в сопровождении (ИМХО), чем рекурсивное. Требовать С++ 17, потому что вариативное using, но вы пометили С++ 17, так что, я полагаю, это не проблема.

max66 13.12.2020 20:54

Так что я на самом деле немного пережевал это, и я разогреваюсь. Это немного более элегантно, и я могу немного больше рассуждать об этом. Мне было немного сложно понять, как используется многоточие (как для использования, так и для наследуемой части). Запустил его через понимание cpp, и это немного яснее. И да, я компилирую с C++ 17, так что все хорошо. Я знаю, что мне нужно продолжать работать над магией шаблонов, но я всегда думаю, что концепции сведут на нет необходимость такой тяжелой работы.

kiss-o-matic 14.12.2020 23:12

Другие вопросы по теме