18

In the codebase I'm working, we use std::any instead of void* to pass classes through some generic non-template code. Specifically, we use Visual Studio 2019, its compiler and standard library.

In order to visualize the std::any, microsoft already gives a natvis:

  <Type Name="std::any">
      <Intrinsic Name="has_value"   Expression="_Storage._TypeData != 0"/>
      <Intrinsic Name="_Rep"        Expression="_Storage._TypeData &amp; _Rep_mask"/>
      <Intrinsic Name="type"        Expression="(const type_info*)(_Storage._TypeData &amp; ~_Rep_mask)"/>
      <Intrinsic Name="_Is_trivial" Expression="has_value() &amp;&amp; _Rep() == 0"/>
      <Intrinsic Name="_Is_big"     Expression="has_value() &amp;&amp; _Rep() == 1"/>
      <Intrinsic Name="_Is_small"   Expression="has_value() &amp;&amp; _Rep() == 2"/>
      <DisplayString Condition="!has_value()">[empty]</DisplayString>
      <DisplayString Condition="_Is_trivial() || _Is_small()">[not empty (Small)]</DisplayString>
      <DisplayString Condition="_Is_big()">[not empty (Large)]</DisplayString>
      <Expand>
          <Synthetic Name="has_value">
              <DisplayString>{has_value()}</DisplayString>
          </Synthetic>
          <Synthetic Name="type" Condition="has_value()">
              <DisplayString>{type()}</DisplayString>
          </Synthetic>
          <Synthetic Name="[representation]" Condition="_Is_trivial()">
              <DisplayString>(Small/Trivial Object)</DisplayString>
          </Synthetic>
          <Synthetic Name="[representation]" Condition="_Is_small()">
              <DisplayString>(Small Object)</DisplayString>
          </Synthetic>
          <Synthetic Name="[representation]" Condition="_Is_big()">
              <DisplayString>(Dynamic Allocation)</DisplayString>
          </Synthetic>
      </Expand>
  </Type>

However, this ends up showing us (Small Object) instead of the std::string that we have stored into it. I've already managed to extend this with a few extra lines to get the pointer to the data:

          <Item Name="[castable_ptr]" Condition="_Is_trivial()">(void*)(&amp;_Storage._TrivialData)</Item>
          <Item Name="[castable_ptr]" Condition="_Is_small()">(void*)(&amp;_Storage._SmallStorage._Data)</Item>
          <Item Name="[castable_ptr]" Condition="_Is_big()">(void*)(_Storage._BigStorage._Ptr)</Item>

However, this shows the data as a void*, which you have to manually cast to a pointer of the actual type std::string*. However, this std::any implementation/visualization also comes with a std::type_info. (see field: type) that knows which underlying type we have.

Is there a way to use this std::type_info so that the (void*) can be replaced by a cast to the actual stored type?

EDIT: An example of the information that visual studio provides for the type: {mydll.dll!class std::tuple<__int64,double,double,double> 'RTTI Type Descriptor'} {...} When explicitly casting the address to std::type_info*, I get access to _Data in the debugger, that contains _DecoratedName (.?AV?$tuple@_JNNN@std@@) and _UndecoratedName (nullptr). Unfortunately, I can't seem to find out how to write a cast that leverages this information.

6502
  • 112,025
  • 15
  • 165
  • 265
JVApen
  • 11,008
  • 5
  • 31
  • 67

2 Answers2

3

I was not aware of the context (implementation constraints). If using std::any is identified as a strong prerequisite (which I can understand) and before Microsoft improves the native vizualizer, below could be a tactical quick win.

In the natvis xml configuration file:

    <Synthetic Name="[representation]" Condition="_Is_trivial()">
        <!--<DisplayString>(Small/Trivial Object)</DisplayString>-->
        <DisplayString>{_Storage._TrivialData._Val}</DisplayString>
    </Synthetic>
    <Synthetic Name="[representation]" Condition="_Is_small()">
        <!--<DisplayString>{*displayStdAnyContent(*this)}</DisplayString>-->
        <Expand>
            <Item Name="[value]">*displayStdAnyContent(*this)</Item>
        </Expand>
    </Synthetic>
    
    <Type Name="AnyCastedValue&lt;*&gt;">
        <DisplayString>{value}</DisplayString>
        <Expand>
            <Item Name="[value]">value</Item>
        </Expand>
    </Type>

C++

//----------------------------------------------------------------------------------
class IAnyValue
{
public:
    virtual ~IAnyValue() = default;
};

template <typename T>
struct AnyCastedValue final :public IAnyValue
{
    AnyCastedValue(const T& pi_value) :value(pi_value) {}

    T value;
};

struct NonCastableValue final :public IAnyValue
{
    static constexpr auto message = "I'm not castable! Please register my type!";
};

//----------------------------------------------------------------------------------
class IAnyCaster
{
public:
    virtual std::unique_ptr<IAnyValue> castValue(const std::any& pi_any) const = 0;
    virtual ~IAnyCaster() = default;
};

template <typename T>
class AnyCaster final :public IAnyCaster
{
public:
    AnyCaster() = default;

    virtual std::unique_ptr<IAnyValue> castValue(const std::any& pi_any) const override
    {
        return std::make_unique<AnyCastedValue<T>>(std::any_cast<T>(pi_any));
    }
};
    
//----------------------------------------------------------------------------------
class AnyCasterService final :public std::unordered_map<std::string, std::unique_ptr<IAnyCaster>>
{
public:
    AnyCasterService()
    {
        //here we register the types you would like be able to watch under the debugger
        registerType<int>();
        registerType<std::string>();
    }

    std::unique_ptr<IAnyValue> castValue(const std::any& pi_any) const
    {
        auto im = find(pi_any.type().name());
        if (im != end())
        {
            return im->second->castValue(pi_any);
        }
        else return std::make_unique<NonCastableValue>();
    }

private:
    template <typename T>
    void registerType()
    {
        this->insert(std::make_pair(typeid(T).name(),std::make_unique<AnyCaster<T>>()));
    }
};

//----------------------------------------------------------------------------------
std::unique_ptr<IAnyValue> displayStdAnyContent(const std::any& pi_any)
{   
    static const AnyCasterService s_service;
    return s_service.castValue(pi_any);
}

I validated this proposal under VS2017 v15.9.8

Keep in mind that Natvis could only evaluate the method at the runtime on your demand (explicit user call).

std::any l_any;
l_any = 5;
l_any = std::string("it works!");

watch

Sedenion
  • 5,421
  • 2
  • 14
  • 42
Jeandey Boris
  • 743
  • 4
  • 9
  • I like the creativity of this solution! It does require me to iterate all types somewhere in code, however it could work. I'll check it out in more detail – JVApen Apr 03 '21 at 07:56
  • As far as I know, natvis debugger is running on a separate process than the one you debug. As a consequence, it can only read the memory and interpret it based on their type. Identifying dynamically the type via the natvis configuration file (.xml) is not possible because it requires to call a method (eg: typeid can not be called in "condition" tag). Calling methods/functions in natvis is very restrictive (idea behind: to no corrupt the debuggee process). For std::any, you need a method which downcasts dynamically the embedded value with respect to natvis restrictions. – Jeandey Boris Apr 03 '21 at 09:46
  • 1
    Keep in mind that my proposal is just a workaround for debugging purpose only! – Jeandey Boris Apr 03 '21 at 09:51
  • Yeah, for a permanent solution we need extensions to natvis – JVApen Apr 03 '21 at 10:27
  • Notice from 2022: The `_Storage._TrivialData._Val` line in the `_Is_trivial` part in the XML should be replaced with `*displayStdAnyContent(*this)`. The variable `_Storage._TrivialData._Val` no longer exists. Also, you are supposed to modify the `std::any` entry in the default natvis file, which is typically here: "C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\Packages\Debugger\Visualizers\stl.natvis". – Sedenion Oct 15 '22 at 09:16
-3

std::any will untype your data. It has to be seen as a secured wrapper of void* within the std. As a consequence, you can't see its typed data through natvis at runtime.

What I would suggest is to redesign your implementation to avoid std::any or void* by relying on a generic interface inherited by a template typed concrete class. eg:

class IData //base interface
{
public:
    ~virtual IData()=default;
}

template <typename T> 
class TypedData:public IData //abstract or concrete
{
    //...
    T m_data;
}

void genericFunction(const IData& pi_data); //instead of void* or std::any

int main()
{
    TypedData<std::string> l_data("my typed data");
    genericFunction(l_data);
    //...
}

With this design, you can define a single natvis entry to manage TypedData

<Type Name="TypedData&lt;*&gt;"> //TypedData<*>
    <DisplayString>{m_data}</DisplayString>
</Type>
Jeandey Boris
  • 743
  • 4
  • 9
  • Although I can agree that your solution can work in specific cases, it doesn't solve cases where you don't have the ability to get rid of std::any – JVApen Apr 02 '21 at 14:28
  • Also important here is that `std::any` ownership takes of what you put in. (By making a copy or doing move) – JVApen Apr 02 '21 at 16:27
  • Lots of dynamic environments (e.g. network protocols, message brokers, etc) require type erasure of some sort. Statically typed stuff can be used when going from business rules to wire, but going back from wire to business rule usually require some sort of type erasure mechanism. Hence the original question. – Jazzwave06 Mar 01 '22 at 23:28