Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linker error when using unordered_map but not map?

Tags:

c++

static

linker

This is a very strange issue. I have two classes: a custom console class (CConsole) and a test class (CHashTableTest) which I've made to play around with maps and unordered_maps to learn how they work.

In my console class I have a public static member variable of CConsole that exposes a static console object to the rest of the project so that I can write to this console when ever I want. This works fine for all of my classes including the test class but only when that test classes uses map and not unordered_map!

The error that I receive is:

error LNK2001: unresolved external symbol "public static class CConsole CConsole:output" (?output@CConsole@@2V1@A)

It comes from the class that calls the methods on the test class and not the test class itself but nothing strange is happening in the calling class, it simply instantiates the CHashTableTest object (passing in a CConsole object) and calls Add and Get on it. It is placed in a separate project but this isn't a problem when I use map as the static member variable has been made external using _declspec(ddlexport).

The solution is setup in Visual Studio 2012, the CConsole and CHashTableTest classes are in a DLL project which is an external reference of a Unit Test project where the calling code exists.

The CConsole and CHashTableTest files:

Console.h

#ifndef _CONSOLE_H_
#define _CONSOLE_H_

#define _CRT_SECURE_NO_DEPRECATE
#include <iostream>
#include <fstream>
#include <string>
#include <ctime>
#include <time.h>

// Defines a set of output modes for the CConsole class. This will affect what output the console will target.
// The default is COUT.
enum _declspec(dllexport) EConsoleMode 
{ 
   // Output sent to a CConsole object will be printed using cout.
   COUT,
   // Output sent to a CConsole object will be printed to a file.
   OUTPUT_FILE,
   // Output sent to a CConsole object will be printed using OutputDebugString.
   OUTPUT_DEBUG_STRING, 
   // Output sent to a CConsole object will be printed using Debug::WriteLine.
   DEBUG_WRITE_LINE, 
   // Output sent to a CConsole object will be printed using Console::WriteLine.
   CONSOLE_WRITE_LINE,
   // Output sent to a CConsole object will not be printed.
   NO_OUTPUT,
};

// An output wrapper class that allows logging and redirecting of log and debugging messages to different
// output targets. 
class _declspec(dllexport) CConsole
{
public:
   static CConsole output;

   // Constructs a CConsole object with a specific console mode, default is COUT.
   CConsole(EConsoleMode mode = COUT, const char* filePath = "C:/output.txt");
   ~CConsole(void);

   // Gets the mode of this CConsole object.
   EConsoleMode GetMode();

   const char* GetFilePath();

   // Sets the output file path of this CConsole object.
   void SetFilePath(const char* filePath);

   void TimeStamp();

   // Logs a message with this specific CConsole object. An indirect call to an operator overload of
   // CConsole << Type
   template <typename T> void Log(const T &message);

protected:
   // The mode of this CConsole object.
   EConsoleMode m_mode;

   // The file path of the output file for this CConsole object.
   const char* m_filePath;
};


// Operator overload of CConsole << Type, queries the mode of the given CConsole object and 
// selects the appropriate output method associated with the mode.
template <typename T>
CConsole operator<< (CConsole console, const T &input) 
{
   switch(console.GetMode())
   {
   case COUT:
      {
         std::cout << input << "\n";
         break;
      }
   case OUTPUT_FILE:
      {
         ofstream fout;
         fout.open (console.GetFilePath(), ios::app);
         fout << input;
         fout.close();
         break;
      }
#if ON_WINDOWS
   case OUTPUT_DEBUG_STRING:
      {
         OutputDebugString(input);
         break;
      }
   case DEBUG_WRITE_LINE:
      {
         Debug::WriteLine(input);
         break;
      }
   case CONSOLE_WRITE_LINE:
      {
         Console::WriteLine(input);
         break;
      }
#endif
   case NO_OUTPUT:
      {
         break;
      }
   default:
      {
         std::cout << input;
         break;
      }
   }
   return console;
}


// Logs a message by calling the operator overload of << on this CConsole object with the message
// parameter.
template <typename T> 
void CConsole::Log(const T &message)
{
   this << message;
}

#endif

Console.cpp

#include "Console.h"

CConsole CConsole::output = *new CConsole(OUTPUT_FILE, "C:/LocalProjects/---/output.txt"); // Known memory leak here, discussed in comments

// Constructs a CConsole object by assigning the mode parameter to the 
// m_mode member variable.
CConsole::CConsole(EConsoleMode mode, const char* filePath)
{
   m_mode = mode;
   m_filePath = filePath;
   TimeStamp();
}


CConsole::~CConsole(void)
{
   //Log("\n\n");
}


// Returns the current mode of this CConsole object.
EConsoleMode CConsole::GetMode()
{
   return m_mode;
}


const char* CConsole::GetFilePath()
{
   return m_filePath;
}

void CConsole::TimeStamp()
{
   if(m_mode == OUTPUT_FILE)
   {
      std::ofstream file;
      file.open (m_filePath, std::ios::app); // 
      std::time_t currentTime = time(nullptr);
      file << "Console started: " << std::asctime(std::localtime(&currentTime));
      file.close();
   }
}

void CConsole::SetFilePath(const char* filePath)
{
   m_filePath = filePath;
}

HashTableTest.h

#ifndef _HASH_TABLE_TEST_H_
#define _HASH_TABLE_TEST_H_

#include <unordered_map>
#include <map>
#include <vector>
#include "Debuggable.h"
#include "Console.h"

using namespace std;

//template class __declspec(dllexport) unordered_map<int, double>;

struct Hasher
{
public:
  size_t operator() (vector<int> const& key) const
  {
      return key[0];
  }
};
struct EqualFn
{
public:
  bool operator() (vector<int> const& t1, vector<int> const& t2) const
  {
     CConsole::output << "\t\tAdding, t1: " << t1[0] << ", t2: " << t2[0] << "\n"; // If I delete this line then no error!
     return (t1[0] == t2[0]);
  }
};

class __declspec(dllexport) CHashTableTest : public CDebuggable 
{
public:
   CHashTableTest(CConsole console = *new CConsole());
   ~CHashTableTest(void);
   void Add(vector<int> key, bool value);
   bool Get(vector<int> key);

private:
   CConsole m_console;
   unordered_map<vector<int>, bool, Hasher, EqualFn> m_table; // If I change this to a map then no error!
};

#endif

Just to be clear, if I change 'unordered_map' above to 'map' and remove the Hasher function object from the template argument list this will compile. If I don't I get the linker error. I haven't included HashTableTest.cpp because it contains practically nothing.

EDIT

I'm not sure if I'm using unordered_map properly here is the implementation for the CHashTableTest class:

HashTableTest.cpp

#include "HashTableTest.h"


CHashTableTest::CHashTableTest(CConsole console) : CDebuggable(console)
{
}


CHashTableTest::~CHashTableTest(void)
{
}

void CHashTableTest::Add(vector<int> key, bool value)
{
   m_table[key] = value;
}

bool CHashTableTest::Get(vector<int> key)
{
   return m_table[key];
}
like image 709
sydan Avatar asked Nov 01 '22 05:11

sydan


1 Answers

When a DLL's generated import library is being linked with an consuming EXE (or another DLL) the declspecs of the items being brought in should be declspec(dllimport) on the receiver-side. When you're building the actual DLL those same items are made available by usage of declspec(dllexport). Your code has the latter, but not the former.

This is normally achieved by a single preprocessor macro that is defined only for your DLL project in the preprocessor section of the compiler configuration. For example in your header(s) that are declaring things exported from your DLL:

#ifndef MYDLL_HEADER_H 

#ifdef MYDLL_EXPORTS 
#define EXPORT __declspec(dllexport) 
#else 
#define EXPORT __declspec(dllimport) 
#endif 

class EXPORT CConsole 
{ 
// stuff here. 
}; 

#endif 

Doing this will tell the compiler when building the DLL to export the class members, including class statics, because MYDLL_EXPORTS is defined in the preprocessor configuration flags for your DLL project.

When building a consuming project, do NOT define MYDLL_EXPORTSin the preprocessor configuration of the compiler settings. It will thusly use dllimport, rather than dllexport. That should square away the linker to know where to look for the things it needs.

Best of luck.

like image 155
WhozCraig Avatar answered Nov 15 '22 03:11

WhozCraig