Search code examples
c++-cli

How to call base class indexer property in c++/cli?


I'm trying to implement a class inheriting from the generic List<T> class:

Class MyList::List<MyClass>

I want to call the List<T>::Item[Int32] indexer property in one of my methods, but I couldn't figure out how to do it.

I tried List<MyClass>::[i] and List<MyClass>::Item[i], but neither works.

I know, to call other methods from List<MyClass>, for example Add, I can just do List<MyClass>::Add(myInstance).

(In my real code, I intend to implement a class that stores a list of images. Depending on some conditions, my indexer would either return the raw image or a processed image. As such I need to include some logic in the getter for the indexer.)


Solution

  • Since List<T>.Item[Int32] is a default indexer, you can just do this[i]. For instance:

    public ref class MyList : System::Collections::Generic::List<MyClass ^>
    {
    public:
        int CountNonNull();
    };
    
    int MyList::CountNonNull()
    {
        int nonNull = 0;
        for (int i = 0; i < Count; i++)
        {
            if (this[i] != nullptr)
                nonNull++;
        }
        return nonNull;
    }
    

    Or if you prefer you can use the "default indexer" property name default[i] explicitly:

    if (this->default[i] != nullptr)
        nonNull++;
    

    List<MyClass ^>::default[i] is also valid and can be used to call the base indexer when overriding the indexer itself.

    See: How to: Use Properties in C++/CLI: Indexed properties.


    In comments you write that you do intend to override the List<T>.Item[Int32] default indexer:

    I intend to implement a class that stores a list of images. Depending on some conditions, my indexer would either return the raw image or a processed image.

    If so, I don't recommend inheriting from List<T> because List<T>.Item[Int32] is not virtual and not intended to be overridden. Thus you will only be able to hide it via interface re-implementation. But this leaves your app open to a possible bug: if your MyList is ever upcast to the base class List<MyClass ^>, then the replacement indexer will not be called. Even worse, it seems that the enumerator returned by List<T>.GetEnumerator() will not call your replacement indexer, so simply iterating through a MyList will result in unprocessed items being returned.

    To see what I mean, consider the following synthetic example:

    ref class MyList;
    
    public ref class MyClass
    {
    public:
        property bool Processed;
    };
    
    public ref class MyList : List<MyClass ^>, IList<MyClass ^>
    {
    public:
       // default indexer
        virtual property MyClass^ default[int] 
        {
            MyClass^ get(int index) new = IList<MyClass ^>::default::get  // hiding + interface reimplementation of default::get method
            { 
                MyClass^ item = List<MyClass ^>::default[index]; // Call base default indexer
                item = ProcessItem(item);                        // Add your custom logic here
                return item;                                     // return the item
            }
            void set(int index, MyClass^ value) new = IList<MyClass ^>::default::set // hiding + interface reimplementation of default::set method
            {
                List<MyClass ^>::default[index] = value;         // Call base default indexer
            }
        }
    
    private:
        MyClass^ ProcessItem(MyClass ^item)
        {
            // Add your custom logic here
            if (item != nullptr)
                item->Processed = true;
            return item;
        }
    };
    

    Here the get method calls ProcessItem() for processing before being returned. (Your image processing would go here; in the example a bool is set to true.)

    Now consider the following unit test:

    MyList ^list = gcnew MyList();
    
    list->Add(gcnew MyClass());
    for each (MyClass^ item in list)
        Debug::Assert(item->Processed); // FAILS because enumerator doesn't call indexer!
    
    list->Add(gcnew MyClass());
    Debug::Assert(list[list->Count-1]->Processed); // Passes because re-implemented Add() was called.
    
    // Upcast to base class
    List<MyClass ^>^ baseList = list;
    baseList->Add(gcnew MyClass());
    Debug::Assert(baseList[list->Count-1]->Processed); // FAILS because re-implemented Add() was NOT called!
    
    // Upcast to interface
    IList<MyClass ^>^ iList = list;
    iList->Add(gcnew MyClass());
    Debug::Assert(iList[iList->Count-1]->Processed); // Passes because re-implemented Add() was called.
    

    While the second and fourth asserts pass, the first and third fail because the replacement get() method is not getting called. And this really can't be avoided; interface re-implementation via the new keyword doesn't actually override the base implementation, it creates a new implementation with a new slot in the vtable. See: new (new slot in vtable) (C++/CLI and C++/CX).

    So, what are your options?

    Firstly, if the necessary image processing can be done when adding the image to the collection, you can inherit from System.Collections.ObjectModel.Collection<T> which provides protected virtual methods that are called whenever items are added or removed from the collection. (This is the base class for ObservableCollection<T>.)

    Thus if we define MyList as follows:

    public ref class MyList : Collection<MyClass ^>
    {
    protected:
        virtual void InsertItem(int index, MyClass ^ item) override
        {
            Collection<MyClass ^>::InsertItem(index, ProcessItem(item));
        }
    
        virtual void SetItem(int index, MyClass ^ item) override
        {
            Collection<MyClass ^>::InsertItem(index, ProcessItem(item));
        }
    
    private:
        MyClass^ ProcessItem(MyClass ^item)
        {
            // Add your custom logic here
            if (item != nullptr)
                item->Processed = true;
            return item;
        }
    };
    

    Then all four asserts now pass:

    MyList ^list = gcnew MyList();
    
    list->Add(gcnew MyClass());
    for each (MyClass^ item in list)
        Debug::Assert(item->Processed); // Passes
    
    list->Add(gcnew MyClass());
    Debug::Assert(list[list->Count-1]->Processed); // Passes
    
    // Upcast to base class
    Collection<MyClass ^>^ baseList = list;
    baseList->Add(gcnew MyClass());
    Debug::Assert(baseList[list->Count-1]->Processed); // Passes
    
    // Upcast to interface
    IList<MyClass ^>^ iList = list;
    iList->Add(gcnew MyClass());
    Debug::Assert(iList[iList->Count-1]->Processed); // Passes      
    

    Secondly, if the image processing must be done in the get() method, you can use the decorator pattern and create own custom collection implementing List<MyClass ^> that wraps an underlying List<MyClass ^> and does the necessary processing in its own default indexer, enumerator, CopyTo() and other methods that access items as required:

    public ref class MyList : IList<MyClass ^>
    {
    private:
        List<MyClass ^> list;
    
    public:
        MyList()
        {
            list = gcnew List<MyClass ^>();
        }
    
        virtual property MyClass^ default[int] 
        {
            MyClass^ get(int index)
            {
                MyClass^ item = list[int];
                item = ProcessItem(item); // Add your custom logic here                
                return item;                                     
            }
            void set(int index, MyClass^ value)
            {
                list[index] = value;
            }
        }
    
        // Implement all other methods as required.
        virtual property int Count { int get() { return list->Count; } }
    
        // Etc
    };
    

    Now the underlying list is contained within your translating list and there is no possibility the "raw" image can get accessed.