2

I'm making a simple doubly-linked list with dynamic memory and templates, it is suposed to recive user input to generate it. Now I want to prompt the user if the list is made of ints, floats, double or chars. Is that posible?

template <class T>
class le
{
    nodo<T>* head = nullptr;
    bool (*comp)(T, T);
public:
    le(bool (*c)(T, T) = ascendente) {
        comp = c;
    }
    void add(T valor);
    void remove(T valor);
    bool find(T bus, nodo<T>*& pos);
    void print();
    ~le();
};

One alternative that I tought was to declare 4 lists for each data type and make a pointer for each list but if there were a direct way to do it it'll save memory and it would be faster.

le<int> leInt;
le<char> leChar;
le<float> leInt;
le<double> leChar;
Magr Cruz
  • 23
  • 5
  • Sure you can. The more interesting question is how do you plan to _use_ this list without knowing its type? – cdhowie Jul 07 '20 at 03:43
  • Also note that you violate the [rule of five](https://en.cppreference.com/w/cpp/language/rule_of_three) here -- since you have a destructor, you should also have a copy and move constructor. Otherwise, any copy will result in shared ownership of the `head` pointer, and an eventual double-free. The simplest way to deal with this would be to change `head` to `std::unique_ptr>` and remove your destructor, then make sure that `nodo` also uses smart pointers for allocations it owns. Then destruction and moving are automatic, and you can just implement copying. – cdhowie Jul 07 '20 at 03:50
  • Perhaps you're looking for a [Class Factory](https://stackoverflow.com/a/16047779/1553090) – paddy Jul 07 '20 at 03:55

3 Answers3

4

The most straightforward way to deal with this would be to use the adapter pattern where the adapters all inherit from an "interface" type (pure virtual methods only). To do this, you need to know what adapted operations you need to perform on each list.

For example, given the single operation "add an item to the list" we can use std::string as the argument type, since this will likely be the type of input anyway. So, our base adapter interface looks like:

class le_adapter {
public:
    virtual ~le_adapter() = default;
    virtual bool add(std::string const &) = 0;
};

Now we need an implementation of the adapter for each list type. Thankfully, templates can help us here:

template <typename T>
class le_adapter_impl : public le_adapter {
public:
    virtual bool add(std::string const &) override;

    le<T> & get();

private:
    le<T> list;
};

template <typename T>
bool le_adapter_impl<T>::add(std::string const & str) {
    // Naive implementation; should do more error checking.
    T value;
    std::istringstream source{str};

    if (source >> value) {
        list.add(value);
        return true;
    }

    return false;
}

template <typename T>
le<T> & le_adapter_impl<T>::get() {
    return list;
}

Now we have an adapter we can put in front of a list of a given type and use it polymorphically through le_adapter without needing to know what kind of list is held within. (Note that a "proper" adapter would store a reference/pointer instead of an object, but having the adapter own the list simplifies this example.)

Now, you can just construct a different adapter depending on the user's input. For example, you could store factory functions in a map:

std::map<std::string, std::function<std::unique_ptr<le_adapter>()>> factories{
    {"int", std::make_unique<le_adapter_impl<int>>},
    {"char", std::make_unique<le_adapter_impl<char>>},
    {"float", std::make_unique<le_adapter_impl<float>>},
    {"double", std::make_unique<le_adapter_impl<double>>},
};

Now you have a map you can use to dispense adapters depending on what the user enters:

std::string input{"int"}; // Example user input

auto i = factories.find(input);
if (i != factories.end()) {
    auto adapter = i->second(); // Creates the desired adapter.

    adapter->add("123"); // This would also be user input
} else {
    // Handle invalid selection
}

Now we have adapter which is std::unique_ptr<le_adapter>. You don't have to know which specific implementation is used in order to use the virtual methods defined on the base interface.

The adapter is only as useful as its base interface; if you need to support other operations then you have to define them on that interface, and they have to have generic arguments or return values to be useful.

If you need to perform extensive manipulation/calculation that would benefit from knowing the type of T then you could use a template function and apply it to the adapter using the visitor pattern through an overloaded functor. (Or cheat and do that in the adapter, though that could be considered to violate the separation of concerns.)

cdhowie
  • 158,093
  • 24
  • 286
  • 300
1

Template parameters must be evaluated at compile time, whereas user input occurs at runtime, so there's no direct way specify a template based on user input.

You will need to explicitly specify multiple versions of your templated class in your code - be that as separately declared variables, separate statements allocating a common pointer, or with some other level of indirection which can then be selected based on user input.

TheNitesWhoSay
  • 167
  • 1
  • 5
  • _"You will need to have multiple declarations and select one of them based on the user input."_ Not quite. There are other patterns that can also work. – cdhowie Jul 07 '20 at 04:05
  • to vary a template argument at runtime you must have declared, directly or indirectly, all usable versions of the templated item at compile time; a different approach may of course apply to creating a doubly-linked list capable of holding different types – TheNitesWhoSay Jul 07 '20 at 04:24
  • I guess it depends what you mean by "declared all usable versions of the templated item." You don't need a variable of each type, for example (I thought this is what you meant). – cdhowie Jul 07 '20 at 04:27
1

You can do what you want by writing a helper function like this.

template <typename T>
void do_work() {
    le<T> list;
    // do stuff
}

int main() {
    if(use_double) {
        do_work<double>();
    }
    else if(use_int) {
        do_work<int>();
    }
}

Obviously you'd need to replace the dummy logic in main with whatever you use to get the type from the user. Since the actual logic would be in do_work and you have a specific type now (T) you don't need multiple le instances floating around. It also means you can add new types by calling do_work with new types (e.g., do_work<le<int>>, if you want a list of lists of ints.).

Stephen Newell
  • 7,330
  • 1
  • 24
  • 28