уменьшить количество специализаций шаблона при использовании указателя члена на константный и неконстантный метод

У меня есть код шаблона, который принимает общий указатель на класс и вызывает функцию или метод. Проблема возникает, если вызываемый метод определен как const.

Пример:

struct Y {}; 
struct X
{
        const Y Go() const { return Y{}; }
        const Y Go2() { return Y{}; }
};

Y f1( std::shared_ptr<X>  ) { return Y{}; }

template< typename FUNC, typename ... ARGS >
auto Do( std::shared_ptr<X>& ptr, FUNC&& f, ARGS&& ... args )
{
    return f( ptr, std::forward<ARGS>(args)... );
}

template < typename CLASS, typename RET, typename ... ARGS>
auto Do( std::shared_ptr<X>& base_ptr, RET (CLASS::*mem_ptr)( ARGS...), ARGS&& ... args )->RET
{
    return (base_ptr.get()->*mem_ptr)( std::forward<ARGS>(args)...);
}

// Any chance to avoid the full duplication of the code here
// to define the member pointer to a const method?

template < typename CLASS, typename RET, typename ... ARGS>
auto Do( std::shared_ptr<X>& base_ptr, RET (CLASS::*mem_ptr)( ARGS...) const, ARGS&& ... args )->RET
{
    return (base_ptr.get()->*mem_ptr)( std::forward<ARGS>(args)...);
}

int main()
{
    auto xptr = std::make_shared<X>();
    Y y1 = Do( xptr, &X::Go );
    Y y2 = Do( xptr, &X::Go2 );
    Y y3 = Do( xptr, &f1 );
}

Моя проблема - последняя специализация с RET (CLASS::*mem_ptr)( ARGS...) const. Я просто хочу остановить дублирование всего кода только для константы. В реальном коде функция снова вызывает другую шаблонную, что приводит к дублированию здесь большого количества кода.

Есть ли шанс избавиться от специализации для указателя члена const?


person Klaus    schedule 14.01.2019    source источник
comment
@ user463035818: Это минимальный пример, и совершенно не имеет значения, нужна ли коду примера эта функциональность или нет. Единственная цель состоит в том, чтобы показать, что существует потребность в версии const, специализированной для указателя на член...   -  person Klaus    schedule 14.01.2019
comment
Связано: stackoverflow.com/questions/24888597/   -  person Holt    schedule 14.01.2019
comment
@Holt Не для этого проекта, но меня также интересует решение на С++ 17. Возможно, когда-нибудь наша компания использует компилятор С++ 17, может быть, начиная с 2025 года... ;)   -  person Klaus    schedule 14.01.2019
comment
Дополнительное примечание для C++17 и вызова. Если f1 взял X& вместо shared-ptr, вы могли бы использовать std::invoke для всех трех перегрузок, поскольку std::invoke(f, r, args... ) вызывает f(r, args) или (r.*f)(args...) в зависимости от f.   -  person Holt    schedule 14.01.2019
comment
Вот другая версия, не требующая SFINAE: godbolt.org/z/8pr6p6   -  person Holt    schedule 14.01.2019


Ответы (3)


Вы можете сделать:

template< typename FUNC, typename ... ARGS >
auto Do( std::shared_ptr<X>& ptr, FUNC&& f, ARGS&& ... args )
-> decltype((f(ptr, std::forward<ARGS>(args)... )))
{
    return f( ptr, std::forward<ARGS>(args)... );
}

template<typename MemberF, typename ... ARGS>
auto Do(std::shared_ptr<X>& base_ptr, MemberF mem_ptr, ARGS&& ... args)
-> decltype((base_ptr.get()->*mem_ptr)( std::forward<ARGS>(args)...))
{
    return (base_ptr.get()->*mem_ptr)( std::forward<ARGS>(args)...);
}
person Jarod42    schedule 14.01.2019
comment
Похоже, это не работает с первой перегрузкой: godbolt.org/z/It-h0b На самом деле это не работает вообще похоже. - person Holt; 14.01.2019
comment
Отличная идея сопоставить в предложении decltype тип возвращаемого значения, чтобы различать указатель и указатель на член. Спасибо! Вы также должны поместить измененную специализацию для указателя функции в свой ответ, так как ссылка на godbolt может умереть, и только та часть, которую вы указали здесь, приведет к неоднозначной перегрузке. Спасибо - person Klaus; 14.01.2019
comment
@Holt: спасибо за исправление. (пытаюсь исправить это с помощью SFINAE на std::is_member_function_pointer на моей стороне). - person Jarod42; 14.01.2019

В C++17 я бы использовал одну шаблонную функцию с if constexpr и проверил, могу ли я вызвать f как шаблонную функцию-член с std::is_invocable или нет, а затем используйте std::invoke для вызова:

template< typename FUNC, typename ... ARGS >
auto Do( std::shared_ptr<X>& ptr, FUNC&& f, ARGS&& ... args ) {
    if constexpr (std::is_invocable_v<FUNC, decltype(ptr), ARGS...>) {
        return std::invoke(f, ptr, std::forward<ARGS>(args)... );
    }
    else {
        return std::invoke(f, ptr.get(), std::forward<ARGS>(args)... );
    }
}

До C++17 у вас может быть две перегрузки: одна для функций, не являющихся членами, и одна для функций-членов. Затем вы можете использовать SFINAE для отключения одного или другого в зависимости от типа вызываемого объекта (используя что-то похожее на std::is_invocable).

person Holt    schedule 14.01.2019

Вот версия C++14, которая не требует SFINAE и опирается на тот факт, что:

  • const Y (X::*)() совпадает с U1 X::* с U1 = const Y();
  • connt Y (X::*)() const совпадает с U2 X::* с U2 = const Y() const.
template< typename FUNC, typename ... ARGS >
auto Do( std::shared_ptr<X>& ptr, FUNC&& f, ARGS&& ... args )
{
    return f( ptr, std::forward<ARGS>(args)... );
}

template < typename CLASS, typename U, typename ... ARGS>
auto Do( std::shared_ptr<X>& base_ptr, U CLASS::*mem_ptr, ARGS&& ... args )
{
    return (base_ptr.get()->*mem_ptr)( std::forward<ARGS>(args)...);
}

Публикация другого ответа, потому что он полностью отличается от первого, и оба интересны (на мой взгляд).

person Holt    schedule 14.01.2019
comment
Довольно интересный подход! Просто удалив parms из типа указателя, мы видим, что постоянство сохраняется... Ух ты! Танки для этого! - person Klaus; 14.01.2019