-1

I am trying to implement a list of data of any type by myself, and I want to print this list based on the data type. At this point, I have tested integers and characters. To print a list of these types, there are two methods print_int and print_char, depending on the data type being displayed. My question is, is it possible to define only one method print that will print depending on the data type of my list without creating a class template (unlike List<type> ...)? I mean without this class definition:

template <class T>
class List{
  T *data;
  List *next;
  ...

-------------------------- UPDATE--------------------------

Finally, I found a solution that gives me the ability to dynamically check the type in C++ (using dynamic_cast, see the accepted answer). Unfortunately, many people mistakenly believe that C++ is severely limited. Which demonstrates my question, to which many, not knowing the answer, put minuses.

Alexander Korovin
  • 1,639
  • 1
  • 12
  • 19
  • 1
    General Rule of Thumb: When the data type changes, but the algorithm doesn't, consider using a `template`. – Thomas Matthews Mar 01 '21 at 23:55
  • 1
    Yes you use templates. You need a print function that accepts any type and it will print that type automatically. It already exists. `operator<<()` – Martin York Mar 01 '21 at 23:56
  • https://stackoverflow.com/users/225074/thomas-matthews Does this mean that the only solution is to avoid `void *` and use the class template? https://stackoverflow.com/users/14065/martin-york - again `operator<<()` needs to convert `void*` – Alexander Korovin Mar 02 '21 at 00:01
  • 5
    Regarding "_undefined reference to `void List::print()`_": This might help: [Why can templates only be implemented in the header file?](https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file) – Ted Lyngmo Mar 02 '21 at 00:18
  • My understanding is that you'll need to have a field that is either a type indicator or a pointer to a print function. – Thomas Matthews Mar 02 '21 at 00:29
  • https://stackoverflow.com/users/225074/thomas-matthews - yes, maybe a new attribute as type indicator is a solution. I first tried going to the same type in `print` – Alexander Korovin Mar 02 '21 at 00:32
  • 4
    It's possible using `void*`, but difficult to get right and make safe. If you absolutely must have the type unknown at compile time and known at runtime, consider using `std::variant` or `std::any` rather than trying to do it all manually. – aschepler Mar 02 '21 at 00:38
  • Thank you @aschepler. `std :: any` is very interesting and might be a solution – Alexander Korovin Mar 02 '21 at 00:50

4 Answers4

3

If you want to store any type in a list without losing type information, you might try a polymorphic approach, too:

class NodeBase
{
public:
    virtual ~NodeBase();
    virtual void print() = 0;

    template <typename T>
    void append(T t);

protected:
    NodeBase* next;

    void appendNode(NodeBase* node);
};

template<typename T>
class Node : public NodeBase
{
public:
    void print() override { ... }

private:
    T data;
};

template <typename T>
void NodeBase::append(T t) // as being template, needs to remain in header!
{
    appendNode(new Node<T>(t));
}

Finally the print implementation might use different overloads of another function to actually print the data value.

Side note: I'd prefer to rename the current List class to Node and make it an inner class of a newly created List class which would manage the nodes internally. The current list implementation can easily result in memory leaks if you move the only pointer to head to a successor of.

Aconcagua
  • 24,880
  • 4
  • 34
  • 59
  • Many thanks! Your answer might look like what I want (especially for the remaining templates in the header). Could you please explain more precisely how to use your Node object? – Alexander Korovin Mar 02 '21 at 10:18
  • @AlexanderKorovin There's not too much to tell about. The new nodes would be used exactly as your variant, the `NodeBase` class would retain all the implementation your original variant hat *but* two details: Storage of the specific data object and the object-specific functionality like printing it. This specific behaviour you make accessible to the base class via virtual functions (a core feature of polymorphism) – and that's it... – Aconcagua Mar 03 '21 at 07:40
  • Thanks again! I ended up making an implementation of a list of different types. But I'm not sure if this is optimal and I have a problem with the implementation of the destructor (which is why I commented it). – Alexander Korovin Mar 03 '21 at 22:30
  • @AlexanderKorovin You might then open a chat room and invite me to – we could discuss your implementation in detail there. – Aconcagua Mar 04 '21 at 23:12
1

Unfortunately, many people mistakenly believe that C++ is severely limited. Which demonstrates my question, to which many, not knowing the answer, put minuses. Luckily, I found a solution/answer that gives me the ability to dynamically check the type in C++ using dynamic_cast. So using if (dynamic_cast<TypeToCheck*>(data)) you can check the data type (is it equal to type TypeToCheck?). Unfortunately, this only works for class types.

I think it's helpful, so here's the implementation:

The list class (list.h):

#pragma once
#include <iostream>

using namespace std;


class Base {
public:
    virtual ~Base() {};
};


class Char : public Base {
    char value;
public:
    Char(char value) : value(value) {};

    friend ostream& operator << (ostream& stream, const Char* self) {
        return stream << "'" << self->value << "'";
    };
};


class Int : public Base {
    int value;
public:
    Int(int value) : value(value) {};

    friend ostream& operator << (ostream& stream, const Int* self) {
        return stream << self->value;
    };
};


class Float : public Base {
    float value;
public:
    Float(float value) : value(value) {};

    friend ostream& operator << (ostream& stream, const Float* self) {
        return stream << self->value;
    };
};


class List
{
    Base* data = nullptr;
    List* next = nullptr;
public:
    List() {};
    List(Base* data):data(data) {};

    void set_next(List* next) {this->next = next;};

    void append(Base* data) {
        List *last_element = this;
        while(last_element->next)
            last_element = last_element->next;

        last_element->set_next(new List(data));
    };

    void print_element(){
        if (dynamic_cast<Char*>(data))
            std::cout << (Char*)data << "(Char)";
        else if (dynamic_cast<Int*>(data))
            std::cout << (Int*)data << "(Int)";
        else if (dynamic_cast<Float*>(data))
            std::cout << (Float*)data << "(Float)";
    }
    void print() {
        bool is_first = true;
        
        cout << endl << "list " << static_cast<void*>(this) << " [";

        List* element = this;
        while(element) {
            if (is_first)
                is_first = !is_first;
            else
                cout << ", ";
            element->print_element();
            element = element->next;
        }
        cout << "]" << endl;
    };
};

Testing file (main.cpp):

#include "list.h"

int main()
{
    List myList(new Int(10));
    myList.append(new Float(1.203f));
    myList.append(new Int(12));
    myList.append(new Char('f'));
    
    myList.print();

    return 0;
}

Result is:

list 000000000014F6D8 [10(Int), 1.203(Float), 12(Int), 'f'(Char)]
Alexander Korovin
  • 1,639
  • 1
  • 12
  • 19
0

That is a very simple list!

template <class T>
class List{
   T*    data;
   List* next;

   void print() {
       std::cout << "[ ";
       print(this, "");
       std::cout << " ]\n";
   }
   void print(List* node, std::string const& sep) {
       if (node == nullptr) {
           return;
       }
       std::cout << sep << node->data;
       print(node->next, ", ");
   }
}

That should work for any type that supports normal stream output. Now this should get you going but I would change how you implement list a lot. Think of a list as the owner of a set of nodes. The nodes hold the data but are not the list.

Martin York
  • 257,169
  • 86
  • 333
  • 562
  • Thanks for the answer. Unfortunately, this is not the case. I don't want to use the class template, because in the case of type `void *` for `data`, we can mix integers, characters, strings, etc. – Alexander Korovin Mar 02 '21 at 00:16
  • 1
    @AlexanderKorovin: In that case, you've erased the type, and therefore should not have any type specific code. – Mooing Duck Mar 02 '21 at 00:41
0

Let's try a different approach: pointers to print functions.
Let's create a synonym for a print function:

typedef (void) (*(Ptr_Print_Function)(std::ostream& output, void * node_data));

Since each node can be of a different type, something will have to initialize the print function correctly.

struct Node
{
  void * p_data;
  Node * prev_node;
  Node * next_node;
  Ptr_Print_Function p_printer;
};

Here are some addition forwarded declarations:

void Print_Integer_Node(std::ostream& output, void * p_data);
void Print_Char_Node(std::ostream& output, void * p_data);  

Here's a code fragment for printing the node to std::cout:

Node * p_node;
//... node search / initialize
(p_node->p_printer)(std::cout, p_node->p_data);

Edit 1: Data Inheritance
IMHO, a cleaner method is to use inheritance and a base (interface) class for the data.

struct List_Data_Interface
{
    virtual void print_data(std::ostream& output) = 0;
    void * p_data;
};

struct Node_Version_2
{
    List_Data_Interface * p_data;
    Node_Version_2 *      p_prev;
    Node_Version_2 *      p_next;
};

In the above model, we are assuming that the data for every node has a print method and a pointer to some data.

A sample integer data:

struct Integer_Data : public List_Data_Interface
{
  void print_data(std::ostream& output)
  {
      int * p_int_data = (int *) p_data;
      output << *p_int_data;
  }
};

Here's some code on how to print a node using the above Node_Version_2:

//...
Node_Version_2 p_node;
//... initialize or find the node.
p_node->print_data(std::cout);

Through polymorphism the correct print function will be chosen.

Thomas Matthews
  • 56,849
  • 17
  • 98
  • 154
  • 1
    IMHO, you should throw away the concept of `void *`, unless as a last resort (like loosing your foot kind of last resort). Use a base interface class instead. A base interface class is easier to use with polymorphism. Let the child class define the functionality of the interface. – Thomas Matthews Mar 02 '21 at 00:51