1

My question is how to make lifetime extension work with CRTP. For example, the following code is perfectly valid:

struct A {
    const int& ref;
};

struct B {
    const A& a;
};

int main() {
    B b{{123}};
    return b.a.ref;
}

Its CRTPed version is not:

template <class DerivedT>
class Gettable {
public:
    int Get() const {
        return static_cast<const DerivedT*>(this)->GetImpl();
    }
};

class A : public Gettable<A> {
    friend class Gettable<A>;
public:
    A(int r) : ref{r}{}

private:
    int GetImpl() const {
        return ref;
    }

    const int& ref;
};

template <class T>
class B {
public:
    B(const Gettable<T>& gettable) : get_{gettable}{}
    int DifferentGet() const {
        return get_.Get();
    }

private:
    const Gettable<T>& get_;
};

int main() {
    B b{A{123}};
    return b.DifferentGet();
}

The problem is that the original A and its Gettable<A> subobject only exist till the the of B constructor.

I have two questions:

1) Why? It is in no way different from the first case of structs, every lifetime is known at compile time, so I believe that compiler should be able to extend lifetime of all temporaries.

2) Is there any good way to overcome this issue?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Nikita Petrenko
  • 1,068
  • 1
  • 7
  • 10
  • 1
    Lifetime extension through const-reference is summarized well in this question, particularly the case you seem to be driving, a `const` reference class member: [Does a const reference class member prolong the life of a temporary?](https://stackoverflow.com/questions/2784262/does-a-const-reference-class-member-prolong-the-life-of-a-temporary). I don't think a CRTP has anything to do with the problem you're having. The temporary from `A{123}` is *gone* by the time `b.DifferentGet()` is reached in `main`. – WhozCraig Jun 10 '19 at 15:32
  • Thanks! Well, I know that it is not specific to CRTP, but I'm asking whether there are any patterns (maybe specific to CRTP) that can help overcome it. – Nikita Petrenko Jun 10 '19 at 15:39
  • 2
    Your first code block is not valid. Unfortunetly (maybe) there is no mechanism in C++ that lets you store a reference to a temporary in a class and have it's lifetime extended. – NathanOliver Jun 10 '19 at 15:40
  • What are you actually trying to accomplish? Knowing that we should be able to suggest an adequate work around. – NathanOliver Jun 10 '19 at 15:40
  • 1
    Nope, it is valid. Take a look at the example in the end https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary – Nikita Petrenko Jun 10 '19 at 15:41
  • Also I believe that the first block is valid because UBsan and Asan are not reacting to it, while complaning on the second one. I don't know how I might be able to describe what I want. Static polymorphism, but it is not a specific specific answer, right? :) My use case almost fully repeats the second block of code. I know that I could use just `T` instead of `const Gettable&`, but It would be effectively duck-typed, and I don't like this idea – Nikita Petrenko Jun 10 '19 at 15:49
  • And replacing it with `T` is also bad because it creates invalid code in multilevel inheritance – Nikita Petrenko Jun 10 '19 at 16:11
  • You link example (in comment) of C++20, but tag it C++17, which one do you want? – Jarod42 Jun 10 '19 at 16:24
  • I meant the very last example, `struct S{int, const std::pair<...>&...};` It seems like C++20 breaks some C++17 code (that's what you read, I think). I'm interested in C++17 – Nikita Petrenko Jun 10 '19 at 16:27
  • *" Is there any good way to overcome this issue?"* you might overload `B(const Gettable&)` and `B(Gettable&&)`. Probably `=delete` the later. – Jarod42 Jun 10 '19 at 16:39
  • I was not right in the previous comment. Now I think they meant that in C++20 you can also initialize with parenthesis – Nikita Petrenko Jun 10 '19 at 16:42

2 Answers2

2

1) Why?

Because there is a function involved - the constructor. The temporary is not bound directly to the member, but rather it is bound directly to the argument of the function - whose lifetime extends until the end of the function, which does not extend beyond the full expression that invoces the function.

It is in no way different from the first case of structs

It is different. There is no constructor involved in aggregate initialisation. In that case, the compiler knows the lifetime of the member, and it knows that the member is initialised with the temporary. The lifetime extension rule applies.

so I believe that compiler should be able to extend lifetime of all temporaries.

Consider following example:

struct foo {};
struct bar {
    bar(const foo& farg);
    const foo& fmem;
};
bar b({});

Should the lifetime of the temporary to extend for the lifetime of b? The standard says, that it doesn't. You appear to be arguing that it should.

Consider following possible implementations of the constructor:

bar::bar(const foo& farg) : fmem{farg} {}         // 1
foo fanother;
bar::bar(const foo& farg) : fmem{fanother} {}     // 2

If the implementation happens to be 1, then you guessed right, the life time extension is needed. If implementation is 2, then we are unnecessarily extending a temporary that is not referred to anymore.

The language designers chose to not extend such temporary, probably so that life times of temporaries don't get extended unnecessarily. As a consequence, implementation 1 is wrong, as well as your CRTP example.

Concisely: The compiler can only extend the lifetime of a temporary until the lifetime of the reference to which the temporary is bound directly. The compiler cannot know what will be done with the reference within the function. It cannot know that an argument has something to do with a member. Those are only known when the constructor is compiled - not when a call to the constructor is compiled.


2) Is there any good way to overcome this issue?

Use either int* or std::reference_wrapper<int> as the constructor argument. Former is more concise, but latter has convenient property of not having a null representation. These should make it harder to accidentally bind a dangling reference. Regardless, document carefully that the referred object must still be valid when Get is called.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • That is a great answer, thank you! So the reason is that member initializer lists are not obliged to be in headers? It is a pity then. Hopefully one day they will come up with something like special types of constructors, which could cover this case – Nikita Petrenko Jun 10 '19 at 16:50
  • I don't think you've come up with a good solution, though. The problem is not in `const int&` -- this part makes no trouble. The real problem is `const Gettable&`, and I have found no way to solve it yet. – Nikita Petrenko Jun 10 '19 at 20:31
0

I believe that the most general solution is something like this. This way it works even for multilevel inheritance.

#include <iostream>
#include <utility>
#include <type_traits>

struct NullType {};

// Helper class for casting
template <class Derived>
class DerivedCaster {
protected:
    Derived* GetDerived() {
        return static_cast<Derived*>(this);
    }

    const Derived* GetDerived() const {
        return static_cast<const Derived*>(this);
    }
};

// Matches the predicate against the types and remembers the first 
// satisfying argument
template <template <class T> class Predicate, class... Args>
struct FindFirstMatching {
    using Type = ... ; // default NullType
    static const bool has_match = ... ;
};

// Structure which gets the deepest class from CRTP inheritance chain
// by looking at the instantiated parent class template
template<typename T>
struct GetDeepest
{
    using Type = T;
};

template<template<class...> class DT, class... T>
struct GetDeepest<DT<T...>>
{
    template <class CLS>
    struct Predicate {
    static const bool value = std::is_base_of<DT<T...>, CLS>::value;
    };

    static const bool HasCRTPDerived = FindFirstMatching<Predicate, T...>::has_match;
    using DerivedT = typename FindFirstMatching<Predicate, T...>::Type;

    using Type = std::conditional_t<HasCRTPDerived, typename GetDeepest<DerivedT>::Type, DT<T...>>;
};

// First abstract class
template <class DerivedT>
class Gettable : public DerivedCaster<DerivedT> {
public:
    int Get() const {
        return DerivedCaster<DerivedT>::GetDerived()->GetImpl();
    }
};

// Second abstract class
template <class DerivedT>
class Incrementable : public DerivedCaster<DerivedT>,
              public Gettable<Incrementable<DerivedT>> {
    friend class Gettable<Incrementable<DerivedT>>;
public:
    int Increment() const {
        return ++(this->Get());
    }

private:
    int GetImpl() const {
        return DerivedCaster<DerivedT>::GetDerived()->GetImpl() + 100;
    }
};

// non-abstract class
class A : public Incrementable<A> {
    friend class Incrementable<A>;
public:
    A(int r) : ref_{r}{}

private:
    int GetImpl() const {
        return ref_;
    }

    int ref_;
};

// Helper to get the copy of the underlying non-abstract class
template <class T>
auto GetDeepestLevelCopy(const T& arg) {
    return static_cast<const typename GetDeepest<T>::Type&>(arg);
}

// Some other class which wants a copy
template <class T>
class B {
public:
    B(const Gettable<T>& gettable) : get_{GetDeepestLevelCopy(gettable)}{}
    int DifferentGet() const {
        return get_.Get();
    }

private:
    typename GetDeepest<Gettable<T>>::Type get_;
};

int main() {
    static_assert(std::is_same_v<GetDeepest<Gettable<Incrementable<A>>>::Type, A>);
    static_assert(std::is_same_v<decltype(GetDeepestLevelCopy(std::declval<Gettable<Incrementable<A>>>())), A>);

    B b{A{123}};
    std::cout << b.DifferentGet() << "\n";
    // prints 223
    return 0;
}

This looks monstrous, but I don't know whether there is a better solution.

Nikita Petrenko
  • 1,068
  • 1
  • 7
  • 10