2

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?

RalphORama
  • 355
  • 1
  • 3
  • 11
  • How about two string arguments and a pointer to an array of string arguments? The array could have one, two, or 47 items in it... – zipzit May 01 '17 at 03:23
  • Possible duplicate of [Variable number of arguments in C++?](http://stackoverflow.com/questions/1657883/variable-number-of-arguments-in-c) –  May 01 '17 at 03:31
  • Use a `std::vector`? – Weak to Enuma Elish May 01 '17 at 03:35
  • @zipzit I already have a constructor that works like that, I use a pointer to an array. @gooroo7 I saw that question earlier, but it doesn't elaborate on how to use that functionality with constructors (or I'm doing something wrong). @JamesRoot I may end up doing that, I was just hoping I could work out a constructor for `Song("Title", "Artist", "Feature 1", "Feature 2", ..., "Feature n");` – RalphORama May 01 '17 at 04:18

5 Answers5

2

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

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
2

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

Community
  • 1
  • 1
skypjack
  • 49,335
  • 19
  • 95
  • 187
  • Upvoted, you managed to condense the TMP gymnastics quite nicely, hadn't seen that trick with is_same and offset variadic sequences. Only downside IMHO (other than complexity/LOC) is that if you do not plan to store the features as-is in a vector, then iterating over them is a little more work (maybe init a std::array of const reference wrappers with the pack?). Another upside of this, is that initializer lists do not allow moves, so in my version there is potentially a redundant `string` copy. – Nir Friedman May 01 '17 at 07:17
  • 1
    @NirFriedman Yes, if you want to iterate over them, you know the size of the pack and can use a `std::array`. I'm glad you like it. ;-) – skypjack May 01 '17 at 07:25
  • @RichardHodges I think both approaches have their merits, but I am curious: the approach I gave is basically the approach used in all of the standard library containers. Do you think that the standard library is mistaken in this approach, or do you think that the situation is different somehow? AFAICS both the initializer list and the variadic solution could have been implemented in 11, but not before, so I don't think it's a matter of chronology. – Nir Friedman May 02 '17 at 10:24
  • @NirFriedman What container uses that approach? I suspect you are confusing things up. Containers accept an `std::initializer_list`, of course, but as their unique argument, not the n-th. Therefore you can do `vec{0, 1, 2}`, that isn't exactly `vec{1, {2, 3}}` or whatever. – skypjack May 03 '17 at 05:38
0

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.

Kryler
  • 101
  • 9
  • I added a constructor, but I get no output. You can find my code for it [here](https://glot.io/snippets/epfvhsi4j0) – RalphORama May 01 '17 at 03:56
0

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.

Rachel Casey
  • 205
  • 2
  • 11
0
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.

Kryler
  • 101
  • 9