trompeloeil icon indicating copy to clipboard operation
trompeloeil copied to clipboard

Detecting parameter count from signature

Open mlimber opened this issue 8 years ago • 14 comments

In this FAQ you ask for ideas on counting function params to avoid MAKE_MOCKn() macros. Following this SO Q&A, it seems like this might work (see it run on Coliru):

#include <iostream>
#include <functional>

template <typename Signature>
struct count_args;

template <typename Ret, typename... Args>
struct count_args<std::function<Ret(Args...)>> {
    static constexpr size_t value = sizeof...(Args);
};

#define GET_COUNT(fn) count_args<std::function<fn>>::value

int main()
{
    std::cout 
        << GET_COUNT(void(int)) << ' '
        << GET_COUNT(int(int,float)) << ' '
        << GET_COUNT(void(int,float,std::string)) << '\n';
}

Which prints "1 2 3". Do you see any issues in this type of solution? (std::function could be factored out, but I leave it in out of laziness for now.) Is this something you think is worth pursuing?

mlimber avatar Jan 27 '17 18:01 mlimber

Unfortunately it's not that simple. For this to work, the number must be available as something that can be used by the preprocessor to generate code from. So, the counting must be done with the preprocessor. I believe that this is not possible, but there are people who are much better at taming that beast than I am, so perhaps there is a way.

The solution you suggested is in fact used in Trompeloeil. It's there to tell you when you've made a mistake ;-)

Try

struct S {
  MAKE_MOCK2(func, void(int,int,int));
};

You'll get a compilation error saying that "Function signature does not have 2 parameters".

rollbear avatar Jan 27 '17 19:01 rollbear

Ah, I see better now. How about something along these lines (on Coliru):

#include <iostream>
#include <boost/preprocessor/seq/size.hpp>
#include <boost/preprocessor/cat.hpp>

#define MAKE_MOCK1 1
#define MAKE_MOCK2 2
#define MAKE_MOCK3 3

#define MAKE_MOCK(name, ret, args) \
  BOOST_PP_CAT( MAKE_MOCK, BOOST_PP_SEQ_SIZE(args) )

int main()
{
    std::cout 
        << MAKE_MOCK(f, void, (int)) << ' '
        << MAKE_MOCK(g, int,  (int)(float)) << ' '
        << MAKE_MOCK(h, void, (int)(float)(std::string)) << '\n';
}

This also prints "1 2 3". I think the syntax could be changed to MAKE_MOCK(h, void, (int, float, std::string) ) with some elbow grease, but I'm just validating the approach with you first.

mlimber avatar Jan 27 '17 19:01 mlimber

You're welcome to try. It'd be very cool if you succeeded. I (strongly) prefer not to need a dependency to boost, but by all means use it while experimenting.

rollbear avatar Jan 27 '17 19:01 rollbear

I think it could be done by pilfering and rebranding a small subset of Boost.Preprocessor with appropriate documentation of the borrowing. Do you think that would be ok license-wise for you? Are you ok with the syntax:

MAKE_MOCK(h, void, (int, float, std::string))
MAKE_MOCK(h, void, (int)(float)(std::string))

where the first would be the goal (I know SQLpp11 does it) but the second could be the fallback position?

mlimber avatar Jan 27 '17 20:01 mlimber

The former I can live with, but the latter I do not like at all.

Might it also be possible to work with MAKE_MOCK(func, (int, int) -> void)? It is legal to write a type signature as auto (int, int) -> void, instead of void(int, int), and prepending the auto is not difficult from macros. Of course, the trailing return type isn't helpful, but it's not needed and it may perhaps be reasonably easy to strip off?

rollbear avatar Jan 27 '17 22:01 rollbear

What are your thoughts about the new functionality to deduce signatures from inherited virtual functions? Functionality is available in v30, tagged just minutes ago.

rollbear avatar Apr 02 '18 13:04 rollbear

Looks good! I wonder if there wouldn't be an SFINAE/if constexpr way to detect the number of params and const-ness too.

mlimber avatar Apr 03 '18 19:04 mlimber

It's trivial (well, almost) to detect the number of parameters with SFINAE constructions, but the catch is that you need the count available as a number that the preprocessor can repeat over. Maybe in a future C++ with reflection + code synthesis. The work on meta classes looks promising for this kind of thing.

rollbear avatar Apr 03 '18 20:04 rollbear

It would be interesting, what your opinion is about the GMock approach to the problem on their master branch: gmock-pp.h

seflue avatar Mar 25 '20 21:03 seflue

Won't any preprocessor-based solution miserably fail on templates? Like std::map<int, double> already looks like not a valid macro argument...

ytimenkov avatar Dec 22 '20 19:12 ytimenkov

I think that this is most probably not an achievable goal at all. Unless we get meta-classes, or some similar reification mechanism, it must be made with the preprocessor, because we cannot invent new identifiers in any other way. However, I'm leaving this open because a lot of the functionality currently in the library are stuff I originally didn't think was possible. Multi-parameter templates, and thus commas, is the least of the problem, more of an inconvenience really. You can work around them, just like you already have to if a return type is a templated type.

rollbear avatar Dec 30 '20 23:12 rollbear

This seems possible if copying Gmocks implementation is an option. Pulling out the basics from https://github.com/google/googletest/blob/main/googlemock/include/gmock/internal/gmock-pp.h results in getting the parameter count easy enough. I don't quite have the format of TROMPELOEIL_MAKE_MOCK_ right but I didn't want to spend too much time on this at work. https://godbolt.org/z/vWP9KeY53

// macros
...

void main() {
    using int_int_pair = std::pair<int, int>;
    std::cout << MAKE_MOCK(foo, void, ()) << std::endl;
    std::cout << MAKE_MOCK(foo, void, (int, char)) << std::endl;
    std::cout << MAKE_MOCK(foo, void, (int, char, int_int_pair), override) << std::endl;
}

$ ./a.out
TROMPELOEIL_MAKE_MOCK_(foo,,0, (), ,,)
TROMPELOEIL_MAKE_MOCK_(foo,,2, (int, char), ,,)
TROMPELOEIL_MAKE_MOCK_(foo,,3, (int, char, int_int_pair), override,,)

ljden avatar Nov 17 '22 03:11 ljden

Seems like this gets most of the way. Requires retval to be separate from the function args. Doesn't handle 0 args, you need to add extra logic to TROMPELOEIL_NARGS to test if __VA_ARGS__ is empty.

#include "trompeloeil.hpp"

#define TROMPELOEIL_NARGS(...) \
    TROMPELOEIL_COUNT(__VA_ARGS__)

#define MAKE_MOCK(name, ret, args, ...) \
    TROMPELOEIL_MAKE_MOCK_(name,,TROMPELOEIL_NARGS args, \
            TROMPELOEIL_IDENTITY(ret)TROMPELOEIL_IDENTITY(args), __VA_ARGS__,,)

class Interface {
    public:
        virtual void foo() = 0;
        virtual int bar(char *) = 0;
        virtual std::set<bool> baz(std::pair<int, char>, void*) = 0;
};

class Mock final : public Interface {
    public:
        MAKE_MOCK0(foo, void(), override);
        MAKE_MOCK(bar, int, (char*), override);
        using pair_int_char = std::pair<int, char>;
        MAKE_MOCK(baz, std::set<bool>, (pair_int_char, void*), override);
};

Types with commas aren't allowed and require aliasing

ljden avatar Nov 19 '22 06:11 ljden

After all these years, there's now experimental support available.

https://trompeloeil.github.io/2024/03/08/Experimental-branch-with-inferred-function-arity.html

Please give it a go and report back.

rollbear avatar Mar 08 '24 19:03 rollbear