3

I started to play with C++ recently. One of the difficulties I have quite often is when the compiler tells that there is an issue with the types: more often than not, those compiler errors look extremely cryptic to me. Here's an example:

Smooth.h:63:15: error: invalid user-defined conversion from ‘Smooth<T, S>::sum(std::size_t) [with T = short unsigned int; long unsigned int S = 20; std::size_t = long unsigned int]::<lambda(short unsigned int, short unsigned int)>’ to ‘short unsigned int (*)(short unsigned int, short unsigned int)

When I worked with C#, I remember there was a way to get quite complicated errors by combining generics, i.e. use generics as parameters of other generics, possibly at more than two levels, but one had to write really WTF code for that, such as:

Action<IEnumerable<Tuple<int?, Func<int?, ICollection<int>, bool>>>

In C++, however, I do have those cryptic errors with code I would consider rather simple itself. So what is happening?

  • Is it a skill to understand the errors given by the compiler?
  • Or I shouldn't get those complicated errors in the first place, and their presence indicates that I'm doing something wrong?

As requested in the comments, here's the corresponding source code, at least the relevant part. Context: the code is intended to run on a microcontroller, so STL is not available, which makes it necessary to reinvent the wheel.

template <typename T, size_t S>
class Smooth {
    T sum(size_t of = S) {
        return this->reduce(
            T(),
            [](T acc, T value) { return acc + value; },
            0,
            of);
    }

    T reduce(
            const T initial,
            T (*acc)(T, T),
            const size_t skip = 0,
            const size_t take = S) {
        ...
    }
};

The actual error was within return acc + value; and happens if T is not an integer, but, for instance, uint16_t. In this case, the lambda result is an integer, while T is expected. The fix consists of replacing the lambda by return static_cast<T>(acc + value);.

10
  • 1
    Could you give an example of your "simple code"? Looks like you might be using heavily templated data structures, which will cause a simple signed/unsigned type mismatch error to look over-the-top complicated. Under the hood, C++ is pretty complicated...
    – Llewellyn
    Oct 12 '20 at 20:21
  • @Llewellyn: I added the source code. Oct 12 '20 at 20:44
  • 2
    A cost of using templates and metaprogramming can mean that the compiler warnings can get quite long. A “simple” program that #includes other code, like <vector>, can mean that your little program is actually thousands of lines of templated code. If you miss-use the library then it’s sometimes difficult to read, as in your example. General rule: ALWAYS pay attention to the first error listed, fix that, then recompile (don’t fix the others). Often one error cascades to make lots of others. Also just getting used to templates and the associated errors makes it less daunting. Best of luck. =]
    – Elliott
    Oct 12 '20 at 21:09
  • Interesting that reduce() takes a function-pointer, instead of a std::function() or any old callable like all the standard algorithms do. Oct 12 '20 at 23:22
  • 2
    16.4.2.4 Freestanding Implementations/Compliance implies it must have the std:: namespace and some minimal library facilities, but doesn't need to provide, say, <algorithm> or <functional>. I don't see any reason not to provide std::plus, but <functional> also includes std::function, which I guess it is reasonable to omit.
    – Useless
    Oct 13 '20 at 11:23
4

Is it a skill to understand the errors given by the compiler?

In general, yes. There's a lot of information packed into it, that you can extract into something more readable with practice.

  1. Smooth.h:63:15: error:

    You have an error on line 63, column 15. I Think this is just the start of the lambda, but any decent editor or IDE will show you.

  2. invalid user-defined conversion from

    It's trying to convert something. We don't have any obvious conversions explicitly in the code, so this is probably the conversion from stateless lambda to function pointer

  3. Smooth<T, S>::sum(std::size_t) [with T = short unsigned int; long unsigned int S = 20; std::size_t = long unsigned int]::<lambda(short unsigned int, short unsigned int)>

    This is the hard-to-read part IMO. Because the lambda type is local to the function, it calls it:

     Smooth<T, S>::sum(std::size_t)::<lambda(short unsigned int, short unsigned int)>
    

    but this is dependent on the outer template parameters, so it inserts those in the middle:

    [with T = short unsigned int; long unsigned int S = 20; std::size_t = long unsigned int]
    

    which makes it much harder to read. Just putting this bit on a separate line would help, and other compilers do this. For comparison, the error from GCC (on trunk, at the time of writing: link) is really verbose, but nicely formatted over several lines

  4. to short unsigned int (*)(short unsigned int, short unsigned int)

    which is the type of the second parameter to reduce.

You are doing something sort-of complex though - mixing function pointers and lambdas is the only reason the conversion is required.

I think in this context it'd be more normal for reduce to be templated on a functor type, so it could take a lambda without conversion, or a function pointer, or a std::function. That way you might get a warning or error inside reduce, if T and the functor return type are incompatible, but it'd be about those specific types.

For example, if you model your method on std::reduce, it would look like

template <class BinaryOp>
T reduce(
        const T initial,
        BinaryOp acc,
        const size_t skip = 0,
        const size_t take = S) {
    // ...
}

(I know you said you can't use the standard library, but you can still look at how it approaches similar issues).

5

The main cause of these sorts of error messages is C++ templates are all applied before you do the regular compiling, whereas most other languages that support it have parametric polymorphism built into the main type system. So, unfortunately, errors that cross that boundary can be infamously cryptic. They are getting better, though. In another 35 years it might be actually readable.

1

It's complaining because it cannot cast a Lambda Object to a pointer to a function.

You nee to capture the function as a plain type (say X) then pass it through to another meta function to decide if its callable.

template <typename X, typename enable_if = void>
struct CallAble
{};

template <typename X>
struct CallAble
   < X
   , std::void_t<decltype(std::declval<X>()(std::declval<T>(), std::declval<T>())>
   >
{
    typedef decltype(std::declval<X>()(std::declval<T>(), std::declval<T>()) type;
};

Will only return a type if its callable, and that type is the result type.

You are going to have to shape this for your specific work.

I figured out the error by reading:

Smooth.h:63:15: error: invalid user-defined conversion from ‘...’ to ‘...’

Pretty clear actually. Something didn't cast. What is it trying to get?

short unsigned int (*)(short unsigned int, short unsigned int)

(*) is the important part. Pointer to a function.

Smooth<T, S>::sum(std::size_t) [with T = short unsigned int; long unsigned int S = 20; std::size_t = long unsigned int]::<lambda(short unsigned int, short unsigned int)>

okay ugly, whats the actual path though

Smooth<T, S>::sum(std::size_t)::<lambda(short unsigned int, short unsigned int)>

Right so a Lambda in the namespace/function Smooth<T, S>::sum(std::size_t).

Yep lambdas don't cast to function pointers.

Helps to have a text editor to cut the error information up in.

3

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.