0

I have the following class:

class Karen { public: Karen(void); ~Karen(void); void complain(std::string level); private: void debug(void) const; void info(void) const; void warning(void) const; void error(void) const; }; 

The complain function receives a string that can contain the words debug, info, warning or error, and it has to call the appropriate function without using a forest of if/elseif/else, using instead pointers to member functions. The prototype of complain is given to me. I am new to pointers to member functions and I am not sure how to manage this. One of my attempts is this:

void Karen::complain(std::string level) { std::string *p = &level; void (Karen::*f)(void) const; (this->*(*p))(); } 

The syntax of the last line is incorrect, but I am trying to do (this->*(content of pointer p))() and I don't know how to write this. Can someone help me?

Edit I am only allowed to use C++98

6
  • 2
    Function names get mangled at compile-time for various reasons, so e.g., Karen::debug is not actually named debug in the end. To that point, you cannot select a function by string name to call at runtime; you need to resolve it at compile-time. Probably your teacher wants your method to receive a function pointer instead of a string, and it's the caller's job to specify the correct one. Commented Aug 4, 2021 at 13:38
  • 2
    If complain is really supposed to receive a std::string, then you are going to need some sort of lookup mechanism to determine which function to call based on the contents of the string. There is no way to convert a string into a pointer to member. Commented Aug 4, 2021 at 13:40
  • 4
    You could have a std::(unordered_)map which would map strings to functions, if that counts as "not being a forest of if/elseif/else". Commented Aug 4, 2021 at 13:41
  • 3
    An enum with a switch would make much more sense in this case... Commented Aug 4, 2021 at 13:43
  • I really wonder why you want to do this. Wouldn't it be easier to make complain a template function? like complain<DEBUG>(...)? Commented Aug 4, 2021 at 15:05

2 Answers 2

4

Syntax to call a member function via member function pointer is

(this->*memf)(); 

You cannot magically turn the string into a member function pointer. Sloppy speaking, names of functions do not exist at runtime. If you want such mapping you need to provide it yourself. No way around that. What you can avoid is the "forest of if-else" by using a std::unordered_map:

#include <unordered_map> #include <string> #include <iostream> class Karen { public: void complain(std::string level) { static const std::unordered_map<std::string, void(Karen::*)() const> m{ {"debug",&Karen::debug}, {"info",&Karen::info}, {"warning",&Karen::warning}, {"error",&Karen::error} }; auto it = m.find(level); if (it == m.end()) return; (this->*(it->second))(); } private: void debug(void) const { std::cout << "debug\n"; } void info(void) const { std::cout << "info\n"; } void warning(void) const { std::cout << "warning\n"; } void error(void) const { std::cout << "error\n"; } }; int main() { Karen k; k.complain("info"); } 

Live Demo

As mentioned in comments, you could use an enum in place of the string. When possible you should use the help of the compiler, which can diagnose a typo in an enum but not in a string. Alternatively you could directly pass a member function pointer to complain. Then implementation of complain would be trivial, no branching needed. Though this would require the methods to be public and the caller would have to deal with member function pointers.


If you are not allowed to use C++11 or newer you should have a serious talk with your teacher. Soon C++20 will be the de facto standard and things have changed quite a lot. I am not fluent in C++98 anymore, so here is just a quick fix of the above to get it working somehow. You cannot use std::unordered_map but there is std::map and initialization of the map is rather cumbersome:

#include <map> #include <string> #include <iostream> class Karen { typedef void(Karen::*memf_t)() const; typedef std::map<std::string,void(Karen::*)() const> map_t; public: void complain(std::string level) { map_t::const_iterator it = get_map().find(level); if (it == get_map().end()) return; (this->*(it->second))(); } private: const map_t& get_map(){ static const map_t m = construct_map(); return m; } const map_t construct_map() { map_t m; m["debug"] = &Karen::debug; m["info"] = &Karen::info; m["warning"] = &Karen::warning; m["error"] = &Karen::error; return m; } void debug(void) const { std::cout << "debug\n"; } void info(void) const { std::cout << "info\n"; } void warning(void) const { std::cout << "warning\n"; } void error(void) const { std::cout << "error\n"; } }; int main() { Karen k; k.complain("info"); } 

Live Demo

Sign up to request clarification or add additional context in comments.

11 Comments

If I use your code, I get the following error: no instance of constructor "std::__1::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map [with _Key=std::__1::string, _Tp=void (Karen::*)() const, _Hash=std::__1::hash<std::__1::string>, _Pred=std::__1::equal_to<std::__1::string>, _Alloc=std::__1::allocator<std::__1::pair<const std::__1::string, void (Karen::*)() const>>]" matches the argument list -- argument types are: ({...}, {...}, {...}, {...})C/C++(289). How can I solve this? P.S. The prototype of complain is given to me, so I cannot pass a member function pointer to it
@kubo did you perhaps forget the const ? The type of the function pointers is void(Karen::*)() const not void(Karen::*)()
@kubo hm code and error message in comments is too difficult to read. It compiles without error here: godbolt.org/z/ev355Yorq.
@kubo the important part of the error message is "argument types are: ({...}, {...}, {...}, {...}" but I think you simplified the important bits out of it
@kubo btw "The prototype of complain is given to me..." when there are such requirements you must mention them in the question explicitly. You now got an answer that answers the question you did ask, but not the one you wanted to ask exactly. This is bad luck, but you also should not modify the question now to make an already existing answer invalid, because thats not nice
|
0

Let us start with the required includes.

#include <cassert> #include <iostream> #include <map> 

Providing the log level as a string, can lead to errors, since the compiler cannot check for typos. Hence an enum::class would be a better choice for determining the log level.

enum class LogLevel { DEBUG, INFO, WARNING, ERROR }; 

C++ does not offer a way to obtain a function pointer given a string. After compiling and linking the function names all have been replaced by their appropriate addresses in memory. Hence, we first need to store the function pointers, in a way that allows us to look them up as needed. For this purpose you can use a static class attribute, and store the function pointers in a std::map.

class Logger { public: Logger(); ~Logger(); void complain(LogLevel level); private: void debug() const; void info() const; void warning() const; void error() const; using HandlerMap = std::map<LogLevel, void (Logger::*)(void) const>; static HandlerMap handlers; }; Logger::HandlerMap Logger::handlers{ {LogLevel::DEBUG, &Logger::debug}, {LogLevel::INFO, &Logger::info}, {LogLevel::WARNING, &Logger::warning}, {LogLevel::ERROR, &Logger::error} }; 

The complain method then just needs to look up the correct function pointer and call the method.

void Logger::complain(LogLevel level) { assert(handlers.find(level) != handlers.end()); (this->*handlers[level])(); } 

The remaining functions look as follows.

Logger::Logger() {} Logger::~Logger() {} void Logger::debug() const { std::cout << "debug" << std::endl; } void Logger::info() const { std::cout << "info" << std::endl; } void Logger::warning() const { std::cout << "warning" << std::endl; } void Logger::error() const { std::cout << "error" << std::endl; } int main(int argc, char* argv[]) { Logger k; k.complain(LogLevel::DEBUG); k.complain(LogLevel::INFO); k.complain(LogLevel::WARNING); k.complain(LogLevel::ERROR); } 

Note, if you insist on using strings, you can replace LogLevel by std::string and LogLevel::<member> by the corresponding string.


The same can be achived using C++98. However, you will need a bit more bootstrapping.

#include <cassert> #include <iostream> #include <map> enum LogLevel { LL_DEBUG, LL_INFO, LL_WARNING, LL_ERROR }; class Logger { public: Logger(); ~Logger(); void complain(LogLevel level); typedef std::map<LogLevel, void (Logger::*)() const> HandlerMap; friend struct LoggerInit; private: void debug() const; void info() const; void warning() const; void error() const; static HandlerMap handlers; }; Logger::HandlerMap Logger::handlers = Logger::HandlerMap(); struct LoggerInit { LoggerInit() { Logger::handlers[LL_DEBUG] = &Logger::debug; Logger::handlers[LL_INFO] = &Logger::info; Logger::handlers[LL_WARNING] = &Logger::warning; Logger::handlers[LL_ERROR] = &Logger::error; } } logger_init; void Logger::complain(LogLevel level) { assert(handlers.find(level) != handlers.end()); (this->*handlers[level])(); } Logger::Logger() {} Logger::~Logger() {} void Logger::debug() const { std::cout << "debug" << std::endl; } void Logger::info() const { std::cout << "info" << std::endl; } void Logger::warning() const { std::cout << "warning" << std::endl; } void Logger::error() const { std::cout << "error" << std::endl; } int main(int argc, char* argv[]) { Logger k; k.complain(LL_DEBUG); k.complain(LL_INFO); k.complain(LL_WARNING); k.complain(LL_ERROR); } 

4 Comments

The prototype of complain is given to me, and it has to take std::string as a parameter
@kubo Then just perform the replacement described in my last sentence.
Plus, I am only allowed to use C++98. I think map and unordered map work for C++11 onwards
@kubo std::map is available in C++98, std::unordered_map is not

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.