Avi
Avi

Reputation: 147

How can you return different types from a function template depending on a condition?

I have the following piece of code:

helper.hpp :

struct A {
   uint32_t a, b;
};

struct B {
  uint32_t a, b;
};

template <typename T>
struct C {
  T barcode;
};

Now based on some condition I want to create appropriate struct object in the main.cpp

if(/* something */) {
  C<A> obj;
}
else {
  C<B> obj;
}

Now the problem is since it's inside the if scope I can't access outside it. One way to handle it would be to return the object from a function, something like this:

template <typename T> 
C<T> getObject(){
  if(/* something */) {
    return C<A>{};
  }
  else{
    return C<B>{};
  }
}

auto obj = getObject()

but this is giving me following compilation error:

error: no matching function for call to 'getObject() note: couldn't deduce template parameter 'T'

Really appreciate any help.

Upvotes: 6

Views: 6100

Answers (4)

Guillaume Racicot
Guillaume Racicot

Reputation: 41840

Types in C++ are determined at compile time. That means that a runtime condition cannot be used to infer the type of an object.

Seeing how you tried to use templates suggest to me that there's a little misunderstanding here. Template code is instanciated by the compiler. Code in templates is not really code until instanciated. If we were to do, by hand, the instanciation of you code using the type A, it would look close to something like this (not actual code):

template <>
auto getObject<A>() -> C<A> {
    if(/* something at runtime */) {
        return C<A>{};
    } else {
        return C<B>{};
    }
}

// auto
C<A> obj = getObject<A>();

As you can see, the code in the else doesn't make sense. You cannot return a value of type C<B> inside a function that must return C<A>. As a side effect of compile time instanciation of code, C<A> and C<B> are unrelated and are completely different types.

Also, you can see that auto has been replaced by C<A>. This is because auto is also inferred at compile time.


Now... What can you do to make your code work?

There are multiple solution and abstraction to have a variable that has a runtime defined type. I'll cover some of the options you can use.

Using a variant

A variant is a class that can hold a single instance of a variable that can be of different types, specified in a finite list of types. For instance, a std::variant<int, std::string> is a variable that can either be a integer or a string.

In your code, it would be a variant of C<A> and C<B>:

auto getObject() -> std::variant<C<A>, C<B>> {
    if (/* something at runtime */) {
        return C<A>{};
    } else {
        return C<B>{};
    }
}

auto obj = getObject();

// The type of obj is std::variant<C<A>, C<B>>

If you don't have access to C++17, you can always use boost::variant.

The downside of this solutions is that you have to know every types the variant can take. If you have a indefinite amount of types, you cannot use variant. It is however, extremely fast and promote regularity ( value semantics ).

virtual polymorphism

Virtual polymorphism is the most common way to get variables of different types decided at runtime. It looks nice but comes with the price of pointer and dynamic allocation, and is rather intrusive. It would look like this:

struct CC {
    virtual ~CC() = default;
};

template<typename T>
struct C : CC {
    T barcode;
};

auto getObject() -> std::unique_ptr<CC> {
    if (/* something at runtime */) {
        return std::make_unique<C<A>>();
    } else {
        return std::make_unique<C<B>>();
    }
}

auto obj = getObject();

// The type of obj is std::unique_ptr<CC>

Note that if this is what you want to do, you have to define some common interface in CC. The rest of the code will use that common interface in order to do operations on C and it's barcode.

Please note that std::make_unique is part of C++14. It can be replaced by std::unique_ptr<C<A>>{new C<A>} and std::unique_ptr<C<B>>{new C<B>} respectively.


There is also std::any and other form of type erasure technique available. This answer is already quite long. There is plenty of documentation you can find online for free that describes all of this in depth.

Upvotes: 10

lllShamanlll
lllShamanlll

Reputation: 184

If condition is runtime

To resolve problem with irrelevant return type you can simply modify code in that way:

either<C<A>, C<B>> getObject() {
  if(/* something */){
    return C<A>{};
  }
  else{
    return C<B>{};
  }
}

either implementation

If condition is compile time

typename std::conditional</* condition */, C<A>, C<B>>::type();

Upvotes: 2

skypjack
skypjack

Reputation: 50568

You can simply do this:

template <typename T> 
C<T> getObject(){
    return {};
}

See it on wandbox.
If you want also to initialize the object, you can do something like this (as long as it has aggregate type as A or B):

template <typename T, typename... A> 
C<T> getObject(A&&... args){
    return { std::forward<A>(args)... };
}

Upvotes: 1

Rakete1111
Rakete1111

Reputation: 49028

I'm still wondering if there is any useful case where you would want something like that... But the only way I can think off is when condition can be evaluated at compile time, as that would enable the compiler to use either of the 2 types.

// 'auto' to let the compiler deduce the correct type at compile time.
auto getObject() {
  // those 2 branches are evaluated at compile time only, which lets the compiler 
  // discard the other branch, making the code valid.
  if constexpr(/*something at compile time*/)
    return C<A>{};
  else
    return C<B>{};
}

Usage is pretty straightforward:

auto obj = getObject();

Upvotes: 5

Related Questions