Sepehr
Sepehr

Reputation: 2111

Storing lambdas as members confusion

I was reading Scott Meyer's Effective Modern C++ and hit the item in which he's suggesting usage of lambdas in place of std::function and std::bind. I understand his argument and his claim about drawbacks of std::function and I agreed with him.

As of today, I decided to switch to templates for storing lambdas (which do not need mutators). I do understand that type of every lambda is only known to the compiler and even two identical lambdas will have different types so how come the following code compiles and works just fine?

template<typename LambdaT>
class CaptureLambda
{
public:
    CaptureLambda(const LambdaT& fn)
        : mActionFn(fn) // initialize mActionFn to a user supplied lambda
    {}

private:
    LambdaT    mActionFn{ []{} }; // initialize mActionFn to an empty lambda
};

My point of confusion is that, how come mActionFn is default initiated to an empty lambda with a different type inside member declarations but the constructor of the class is happily accepting another type of lambda in its arguments? are they castable to each other? if yes, why the following makes the compiler sad?

// Class stuff...

template<typename T>
void resetActionFn(const T& newFn) { // setter member
    mActionFn = newFn;
}

// Class stuff...

Upvotes: 3

Views: 204

Answers (1)

user743382
user743382

Reputation:

Non-static data member initialisers only get used if the constructor doesn't specify a different initialiser.

Given

struct S {
  int i = 3;
  S() : i(4) { }
};

you don't get a default constructor that first initialises i to 3, and then re-initialises it to 4, you just get a constructor that initialises i to 4.

It's the same with your class. You don't have any constructor that doesn't initialise mActionFn, so the initialiser doesn't ever get used.

Now, as Piotr S. points out in the comments, in general, the intialiser must still be semantically valid, but your data member has a dependent type, so the validity cannot be checked at template definition time, and the initialiser never gets instantiated since it is unused, so the error goes undetected at instantiation time as well. A similar simpler example is

template <typename T>
struct S {
  T x = 3;
  S() : x(0) { }
};
int main() {
  S<void*>();
}

which is accepted silently by GCC even though 3 is an invalid initialiser for a field of type void*. clang rejects it though. The standard is unclear on whether the instantiation of a template causes the instantiation of any unused NSDMIs, and some compilers instantiate them only as needed. There is agreement that they should be instantiated only as needed, but there are some problems with that approach, which is why not all compilers implement that yet.

Upvotes: 4

Related Questions