C++ Logo

std-proposals

Advanced search

Re: call (invoke) member function or static member function of a class in generic context.

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
Date: Tue, 7 Jul 2020 17:09:39 -0400
See
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0312r1.html "Making
Pointers to Members Callable" (Barry Revzin, 2017)
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1695.html "A
Proposal to Make Pointers to Members Callable" (Peter Dimov, 2004)

–Arthur



On Tue, Jul 7, 2020 at 4:36 PM Nikolay Mihaylov via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> I know I emailed already for this, but I have another example of how
> *invoke_class_member* dramatically :) reduces the code repetition.
>
> Here is the example:
>
> First function itself:
>
> I had code like this.
>
> template<class PROTOCOL, class DB_ADAPTER, class CONNECTION>
>> class KeyValueWorkerProcessor{
>> // ...
>>
>> WorkerStatus do_save(){
>> const auto &p = protocol_.getParams();
>> if (p.size() != 1)
>> return err_BadRequest_();
>> db_.save();
>> protocol_.response_ok(buffer_);
>> return WorkerStatus::WRITE;
>> }
>> WorkerStatus do_reload(){
>> const auto &p = protocol_.getParams();
>> if (p.size() != 1)
>> return err_BadRequest_();
>> db_.reload();
>> protocol_.response_ok(buffer_);
>> return WorkerStatus::WRITE;
>> }
>> // ...
>
> DB_ADAPTER db_;
>
> };
>
>
> DB_ADAPTER is an unknown type. We only know it has two methods -
> *DB_ADAPTER::save()* and *DB_ADAPTER::reload()*.
> We do not know if these are normal methods or static methods.
> Because of that, we need to repeat the whole function, and only difference
> is *db_.reload();* vs *db_.save();*
>
> now we can do this:
>
> template<class PROTOCOL, class DB_ADAPTER, class CONNECTION>
>> class KeyValueWorkerProcessor{
>> // ...
>>
>> template<typename F>
>> WorkerStatus do_save_(F func){
>> const auto &p = protocol_.getParams();
>> if (p.size() != 1)
>> return err_BadRequest_();
>> // db_."func"();
>> invoke_class_member(db_, func);
>> protocol_.response_ok(buffer_);
>> return WorkerStatus::WRITE;
>> }
>> WorkerStatus do_save(){
>> return do_save_(&DB_ADAPTER::save);
>> }
>> WorkerStatus do_reload(){
>> return do_save_(&DB_ADAPTER::reload);
>> }
>> // ...
>> DB_ADAPTER db_;
>> };
>
>
> Well now it is a *beautiful code* :)
> No repetition and easy to understand even from a novice C++ programmer -
> he / she will immediately understand what db_save() and db_reload() do.
>
> Nikolay
>
> On Mon, May 25, 2020 at 6:51 PM Nikolay Mihaylov <nmmm_at_[hidden]> wrote:
>
>> My naming skills are awful. Naming things is one of two hardest things in
>> programming :)
>>
>> *Another point -*
>>
>> I have proof of concept LinkList implementation I made several years ago.
>> Iterators there there looks like:
>>
>> class LinkList{
>> //...
>> auto begin() const -> iterator{
>> return head_;
>> }
>>
>> constexpr *static* auto LinkList::end() -> iterator{
>> return nullptr;
>> }
>> }
>>
>> Since end() does not require anything from the object (or class) it is
>> marked static.
>>
>> Now suppose some fancy programmer decides to do pointer to member
>> function - begin() or end().
>> How is this possible with the standard library?
>>
>> - begin() have 1 parameter - e.g. instance of the class (because it
>> accesses private members, e.g. head_ )
>> - end() have 0 parameters.
>>
>> With the current standard library there is no way this can be done except
>> "do it yourself".
>>
>>
>>
>> On Mon, May 25, 2020 at 5:42 PM Garrett May <garrett.ls.may_at_[hidden]>
>> wrote:
>>
>>> That is fine - all of that makes sense.
>>>
>>> However, I for one do not believe that such a class_invoke function is
>>> necessary to add to the standard library, in my opinion. It looks from my
>>> point of view to be required only in a specific situation where:
>>> - you need to call a member function pointer, because you require the
>>> (potentially private/protected) information inside the member; and
>>> - you need to call a function pointer, because you don't need any
>>> information from a member
>>>
>>> From a stylistic perspective, class_invoke sounds like an odd name. It
>>> makes sense when you are calling a function pointer that resides inside a
>>> type, but:
>>> - for a member function pointer, you aren't calling a function of a
>>> class - rather, a function of an instance of a class
>>> - for a function pointer that doesn't reside inside a type, there is no
>>> class - so it is also odd for it to be called by class_invoke
>>>
>>> And I think that highlights my point; function pointers and member
>>> function pointers are different types. Only in rare circumstances do I
>>> believe that the standard library should handle both generically - such as
>>> in std::invoke, which can handle other callable cases. This sounds more
>>> like a specific use case.
>>>
>>> (N.B. Nikolay Mihaylov, duplicate reply due to lack of std-proposals in
>>> list of emailees)
>>>
>>>
>>> On Mon, 25 May 2020 at 14:54, Nikolay Mihaylov <nmmm_at_[hidden]> wrote:
>>>
>>>> You are correct, this was my first implementation. I think "constexpr
>>>> if" is not very readable and I usually prefer tag dispatch.
>>>>
>>>> About the arguments - yes, those are different types, but suppose the
>>>> class is passed from somewhere, here is my real code:
>>>>
>>>> template<class PROTOCOL, class DB_ADAPTER, class CONNECTION>
>>>>> class KeyValueWorkerProcessor{
>>>>> // ...
>>>>> template<typename F>
>>>>> WorkerStatus do_accumulate_(F func){
>>>>> const auto &p = protocol_.getParams();
>>>>> if (p.size() != 4)
>>>>> return err_BadRequest_();
>>>>> const auto &key = p[1];
>>>>> uint16_t const count = from_string<uint16_t>(p[2]);
>>>>> const auto &prefix = p[3];
>>>>> protocol_.response_strings(buffer_, *class_invoke(db_, func,
>>>>> key, count, prefix)* );
>>>>> return WorkerStatus::WRITE;
>>>>> }
>>>>> auto do_getx(){
>>>>> return do_accumulate_(&DB_ADAPTER::getx);
>>>>> }
>>>>> auto do_count(){
>>>>> return do_accumulate_(&DB_ADAPTER::count);
>>>>> }
>>>>> auto do_sum(){
>>>>> return do_accumulate_(&DB_ADAPTER::sum);
>>>>> }
>>>>> auto do_min(){
>>>>> return do_accumulate_(&DB_ADAPTER::min);
>>>>> }
>>>>> auto do_max(){
>>>>> return do_accumulate_(&DB_ADAPTER::max);
>>>>> }
>>>>> // ...
>>>>> private:
>>>>> PROTOCOL &protocol_;
>>>>> DB_ADAPTER &db_;
>>>>> CONNECTION &buffer_;
>>>>> };
>>>>>
>>>>
>>>> My first implementation had do_accumulate_() pasted 5 times - It was
>>>> OK, but very long and code duplication.
>>>>
>>>> Second implementation was with lambdas. It was rather confusing and
>>>> hard to read.
>>>>
>>>> Third implementation used direct code - "(db_.*func)(key, count,
>>>> prefix);", however I needed to remove "static" from my Mock adapter.
>>>> That was OK, but I thought - what if later I really need to call a
>>>> static method. Then I will need to do it as "normal" method and hope the
>>>> optimizer cleans up the code for me.
>>>>
>>>> Current implementation works and looks very nice and clear, isn't it?
>>>>
>>>> Nick
>>>>
>>>>
>>>> On Mon, May 25, 2020 at 3:37 PM Garrett May via Std-Proposals <
>>>> std-proposals_at_[hidden]> wrote:
>>>>
>>>>> Are these not different types? One is a member function pointer, which
>>>>> requires two arguments - a Real* and an int - whereas the other is a
>>>>> function pointer which requires one argument - an int;
>>>>>
>>>>> You can do the following:
>>>>>
>>>>> template<typename C, typename F>
>>>>> auto user(C &c, F func){
>>>>> if constexpr(std::is_member_function_pointer<F>::value){
>>>>> return (c.*func)(5);
>>>>> } else{
>>>>> return func(5);
>>>>> }
>>>>> }
>>>>> And subsequently call via:
>>>>>
>>>>> *user(real, &Real::inc);*
>>>>>
>>>>> *user(real, Mock::inc);*
>>>>>
>>>>> On Mon, 25 May 2020, 1:16 pm Nikolay Mihaylov via Std-Proposals, <
>>>>> std-proposals_at_[hidden]> wrote:
>>>>>
>>>>>> Hello,
>>>>>>
>>>>>> suppose you have following clases:
>>>>>>
>>>>>> struct Mock{
>>>>>>> static int inc(int){
>>>>>>> return 0;
>>>>>>> } static int dec(int){
>>>>>>> return 0;
>>>>>>> }
>>>>>>> };struct Real{
>>>>>>> Real(int v) : v(v){} int inc(int a) const{
>>>>>>> return a + v;
>>>>>>> } int dec(int a) const{
>>>>>>> return a - v;
>>>>>>> }private:
>>>>>>> int v;
>>>>>>> };template<typename C, typename F>
>>>>>>> auto user(C &c, F func){
>>>>>>> return (c.*func)(5);
>>>>>>> }
>>>>>>
>>>>>> This will allow to call:
>>>>>>
>>>>>>
>>>>>> *user(real, &Real::inc);*
>>>>>> but will not allow to do
>>>>>>
>>>>>> *user(real, &Mock::inc);*
>>>>>>
>>>>>> However because of generic context, both calls must be possible.
>>>>>>
>>>>>> Here is simple implementation of the feature:
>>>>>>
>>>>>> #include <type_traits>
>>>>>>>
>>>>>>> namespace class_invoke_impl_{
>>>>>>> template <class T, class F, class... Args>
>>>>>>> constexpr auto class_invoke_(T &&cl, F func, std::true_type,
>>>>>>> Args&&... args){
>>>>>>> return
>>>>>>> (std::forward<T>(cl).*func)(std::forward<Args>(args)...);
>>>>>>> }
>>>>>>
>>>>>>
>>>>>>
>>>>>> template <class T, class F, class... Args>
>>>>>>> constexpr auto class_invoke_(T const &, F func, std::false_type,
>>>>>>> Args&&... args){
>>>>>>> return func(std::forward<Args>(args)...);
>>>>>>> }
>>>>>>> }
>>>>>>
>>>>>>
>>>>>>
>>>>>> template <class T, class F, class... Args>
>>>>>>> constexpr auto class_invoke(T &&cl, F func, Args&&... args){
>>>>>>> using namespace class_invoke_impl_;
>>>>>>> return class_invoke_(std::forward<T>(cl), func,
>>>>>>> std::is_member_pointer<F>{}, std::forward<Args>(args)...);
>>>>>>> }
>>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Std-Proposals mailing list
>>>>>> Std-Proposals_at_[hidden]
>>>>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>>>>
>>>>> --
>>>>> Std-Proposals mailing list
>>>>> Std-Proposals_at_[hidden]
>>>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>>>>
>>>> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2020-07-07 16:13:06