Ralph
Ralph

Reputation: 385

How do I pass a variable amount of arguments to a constructor?

I'd like to write a constructor for one of my classes that takes a variable number of std::string arguments after a certain set of predefined arguments.

My current code looks like this:

// Audio.h
#include <string>
class Audio {
public:
    Audio(std::string title, std::string author);
protected:
    std::string title, author;
}

// Song.h
#include "Audio.h"
class Song : public Audio {
public:
    Song(std::string title, std::string artist) : Audio(title, artist);
    // I have working code to populate mFeatures from features
    Song(std::string title, std::string artist, std::string *features) 
        : Audio(title, artist);
private:
    std::string *mFeatures;
}

So I have a constructor that takes string, string and one that takes string, string, *string. I'd like to write one that takes string, string followed by an arbitrary number of strings to populate the mFeatures with.

I've looked around and found this question and this question which lay out the idea with member functions, but I haven't found an answer related to constructors.

TL;DR: Is there a way I can create a constructor that takes two string arguments followed by an arbitrary number of string arguments?

Upvotes: 2

Views: 187

Answers (5)

skypjack
skypjack

Reputation: 50540

Despite the fact that the answer of @NirFriedman is correct, I don't like that much the form below:

Song s("title", "artist", {"f1", "f2"});

I would rather use a form like this:

Song s("title", "artist", "f1", "f2");

That is almost the same but for the fact that you don't have to use an std::initializer_list.

C++17 version (see it on wandbox):

#include<string>
#include<vector>
#include<type_traits>

class Audio {
public:
    Audio(std::string title, std::string author)
        : title{title}, author{author} {}
protected:
    std::string title, author;
};

class Song : public Audio {
public:
    template<typename... T>
    Song(std::string title, std::string artist, T... features)
        : Audio(title, artist), mFeatures{features...}
    {
        static_assert(std::conjunction_v<std::is_convertible<T, std::string>...>);
}

private:
    std::vector<std::string> mFeatures;
};

int main() {
    Song song{"foo", "bar", "..."};
}

C++14 alternative (see it on wandbox):

class Song : public Audio {
    template<bool... B>
    static constexpr bool check = std::is_same<
        std::integer_sequence<bool, true, B...>,
        std::integer_sequence<bool, B..., true>
    >::value;

public:
    template<typename... T>
    Song(std::string title, std::string artist, T... features)
        : Audio(title, artist), mFeatures{features...}
    {
        static_assert(check<std::is_convertible<T, std::string>::value...>, "!");
    }

private:
    std::vector<std::string> mFeatures;
};

The advantage of a static_assert is that it will help printing a more user-friendly error message in case of errors. Note that in the example above also the assignment mFeatures{features...} gives you an error. Anyway you could decide to elaborate the features... within the body of the constructor and to stop the compilation as soon as an inconsistency is detected is usually better

Upvotes: 2

Kryler
Kryler

Reputation: 111

template <typename ...List>
Song(const std::string& title, const std::string& artist, List&&... features) : Audio(title, artist)

And then just use the features formal as a parameter to a private member function that sets the features member just like the example you linked. The best answer in that thread you linked works for you too.

Upvotes: 0

Nir Friedman
Nir Friedman

Reputation: 17704

The best way to do this is to use initializer lists.

// Song.h

#include "Audio.h"
class Song : public Audio {
public:
    Song(std::string title, std::string artist, std::initializer_list<std::string> features) : Audio(title, artist), mFeatures(features) {};
private:
    std::vector<std::string> mFeatures;
};

Usage:

Song s("title", "artist", {"f1", "f2"});

They can be iterated over. More information here: http://en.cppreference.com/w/cpp/utility/initializer_list. Also, you should not be passing title and artist by value (unless you want to learn about std::move), pass them by const reference.

Do not use a pointer to a std::string to hold onto a dynamic array of strings; you're likely to leak memory that way, and there are all kind of other issues. Just use a vector.

Live example: http://coliru.stacked-crooked.com/a/0e8914411dba674f

Upvotes: 2

Rachel Casey
Rachel Casey

Reputation: 225

You should pass string, string and then a vector of strings. Store the arbitrary number of strings in the vector. This is probably the best way to go about this.

Upvotes: 0

Kryler
Kryler

Reputation: 111

Templates work on Constructors too and by extension so do variadic templates. If you tried it already and it didn't work, check how you're compiling it.

Upvotes: 0

Related Questions