1

Can this be made to display the line number of the actual call site, like the macro does ?

#include <iostream>
#include <type_traits>
#include <source_location>
#include <cassert>
using namespace std;

namespace myLib{

template<typename Enumt, typename... Ts> // alert bad calls, throw
constexpr void setValsF(const unsigned line, Ts const&... args){
  cout << "line #"<<line<<": illegal call to setVals()\n";
  assert(is_enum_v<Enumt>);
}

template<class... Enumt, class = common_type_t<Enumt...> > 
requires conjunction_v<is_enum<Enumt>...>
constexpr void setValsF(const unsigned line, Enumt&... vals){
  cout << "line #"<<line<<": legal call to setVals()\n";
}
 
}// myLib

int main(){

  enum enumVals : short { a = 0, b, c, count };
  
  // #define setVals(...) myLib::setValsF<enumVals>(__LINE__ __VA_OPT__(,__VA_ARGS__))
  constexpr auto setVals = [](auto&&... args) {  
    myLib::setValsF<enumVals>(source_location::current().line(), args...); };
  
  setVals(a,b); // legal
  setVals(b,"text"); // illegal
}

run

.. keeping a nice simple API ( setVals(a,b) ), that is.

It would work :

  • with a defaulted argument to the function template, but for the parameter pack. Couldn't figure a deduction guide in the presence of more than one template parameter.
  • or if current() was the default constructor to source_location !

I resorted to runtime handling of bad calls (to certain APIs) as a courtesy to my library users, as my very specific insult is wayyy nicer than the indecipherable blob the compiler vomits.

9
  • The normal trick is to put the default argument into a parameter's constructor. Commented Jan 31, 2023 at 22:08
  • Yup. That's the nicest way out. Could not adapt FormatWithLocation from stackoverflow.com/questions/57547273/… to my case with multiple template parameters :( Commented Jan 31, 2023 at 22:17
  • 3
    I'm not sure why you can't just use static_assert() if all that you want to do is to display error message... Commented Jan 31, 2023 at 22:29
  • 2
    Would this: godbolt.org/z/GrY6njMda be enough or is it too verbose for your taste? Commented Feb 1, 2023 at 14:58
  • 1
    @Bob__ Hmm, clang seems to mess it up in general when source_location is a default argument to function templates. godbolt.org/z/xvxMj3esz - Edit: It doesn't matter if it's a function template. It's borked either way: godbolt.org/z/sd5e46bxG - and I wouldn't care about that too much. They will have to fix that bug. - Edit2: It is fixed, on trunk: godbolt.org/z/5jj5bcP1x Commented Feb 1, 2023 at 22:03

1 Answer 1

1

Expanding on my comment, as requested by the OP.

Piotr Skotnicki's accepted answer to How to use source_location in a variadic template function? introduces an helper class and a deduction guide:

template <typename... Ts>
struct debug
{    
   debug( Ts&&... ts
        , const std::source_location& loc = >std::source_location::current());
};

// The deduction guide forces the deduction of the pack, "separating"
// it from the default parameter.
template <typename... Ts>
debug(Ts&&...) -> debug<Ts...>;

int main()
{
   debug(5, 'A', 3.14f, "foo");
}

Here, though, something else needs to be passed. My proposal is to inject the requirements as a template parameter before the pack.

#include <concepts>
#include <iostream>
#include <source_location>

template <typename R, typename... Ts>
struct debug
{    
  debug( R&& req      // <- A requirement that the pack ts must fulfill.
       , Ts&&... ts
       , std::source_location const& loc = std::source_location::current() )
  {
    std::cout << "line #" << loc.line() << ": "
              << ( req(ts...) ? "valid call\n" : "invalid call\n" );
  }
};

// We don't need to differentiate the first parameter of the pack, here.
template <typename... Args>
debug(Args&&...) -> debug<Args...>;

int main()
{
  enum enumVals : short { a = 0, b, c, count };
  
  // Since C++20 we can have templated lambdas. Here I don't need named parameters,
  // but I need named types to use std::same_as in the fold expression.
  constexpr auto all_enums = []<typename... Ts>(Ts&&...) {
    return ( ... and std::same_as<std::remove_cvref_t<Ts>, enumVals> );
  };

  debug(all_enums, a, b);   // -> line #31: valid call
  debug(all_enums, b, 1);   // -> line #32: invalid call
}

This is not as terse as setVals(a,b), in OP's code, but seems to work1 as intended.


1) Note that this works with gcc and clang(trunk) (as noted by Ted Lyngmo), but not with clang15: https://godbolt.org/z/zxxMTs4Kj

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

4 Comments

Thank you, I'm learning from this. First time I see a unary left fold in the wild. I'm amazed that you can pass a closure not by argument, but by template argument & not only save a std::invoke, but also an ugly .operator() call ! This also elegantly transmits only types to the source_location-instantiating object thus completely eliding the issue of copying an argument of a type 10k x 10k matrix just to tell the user he passed the wrong one ! Brilliant.
Still can't move this closer to target (transparent API call), example : godbolt.org/z/56qzd74Gb
@ExpertNoob1 Yeah, sorry, I can't think of anything better.
Nothing to be sorry for. I don't think C++20 source_location can do this, honestly. Nothing wrong with __LINE__. I already used what I learned from you to reduce the arg checking code by 50% : my API is now single instance taking a universal Ts... squeezed through (... and checkArg.operator()<Is>()); (Is = seq. 0 .. sizeof...(Ts)-1) : much smaller, clearer, nicer, self contained <cool dude emoji>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.